diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc
index 65f7a378ec244..e00a67f6c78a4 100644
--- a/docs/apm/troubleshooting.asciidoc
+++ b/docs/apm/troubleshooting.asciidoc
@@ -49,7 +49,7 @@ GET /_template/apm-{version}
*Using Logstash, Kafka, etc.*
If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka),
then the index template will not be set up automatically. Instead, you'll need to
-{apm-server-ref}/_manually_loading_template_configuration.html[load the template manually].
+{apm-server-ref}/configuration-template.html[load the template manually].
*Using a custom index names*
This problem can also occur if you've customized the index name that you write APM data to.
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index 89330d2a86f76..dfffdffb08a08 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -123,7 +123,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory
interface is to define a way to retrieve a context-based logger instance. |
| [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. |
| [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata |
-| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | |
+| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. |
| [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | |
| [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. |
| [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md
new file mode 100644
index 0000000000000..6f05526b66c83
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md)
+
+## MetricsServiceSetup.collectionInterval property
+
+Interval metrics are collected in milliseconds
+
+Signature:
+
+```typescript
+readonly collectionInterval: number;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md
new file mode 100644
index 0000000000000..61107fbf20ad9
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md)
+
+## MetricsServiceSetup.getOpsMetrics$ property
+
+Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property.
+
+Signature:
+
+```typescript
+getOpsMetrics$: () => Observable;
+```
+
+## Example
+
+
+```ts
+core.metrics.getOpsMetrics$().subscribe(metrics => {
+ // do something with the metrics
+})
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md
index 0bec919797b6f..5fcb1417dea0e 100644
--- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md
+++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md
@@ -4,8 +4,18 @@
## MetricsServiceSetup interface
+APIs to retrieves metrics gathered and exposed by the core platform.
+
Signature:
```typescript
export interface MetricsServiceSetup
```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md) | number
| Interval metrics are collected in milliseconds |
+| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics>
| Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start
phase, and a new value every fixed interval of time, based on the opts.interval
configuration property. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md
new file mode 100644
index 0000000000000..25125569b7b38
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) > [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md)
+
+## OpsMetrics.collected\_at property
+
+Time metrics were recorded at.
+
+Signature:
+
+```typescript
+collected_at: Date;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md
index d2d4782385c06..9803c0fbd53cc 100644
--- a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md
+++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md
@@ -16,6 +16,7 @@ export interface OpsMetrics
| Property | Type | Description |
| --- | --- | --- |
+| [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) | Date
| Time metrics were recorded at. |
| [concurrent\_connections](./kibana-plugin-core-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections']
| number of current concurrent connections to the server |
| [os](./kibana-plugin-core-server.opsmetrics.os.md) | OpsOsMetrics
| OS related metrics |
| [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics
| Process related metrics |
diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md
new file mode 100644
index 0000000000000..095c45266a251
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) > [cpu](./kibana-plugin-core-server.opsosmetrics.cpu.md)
+
+## OpsOsMetrics.cpu property
+
+cpu cgroup metrics, undefined when not running in a cgroup
+
+Signature:
+
+```typescript
+cpu?: {
+ control_group: string;
+ cfs_period_micros: number;
+ cfs_quota_micros: number;
+ stat: {
+ number_of_elapsed_periods: number;
+ number_of_times_throttled: number;
+ time_throttled_nanos: number;
+ };
+ };
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md
new file mode 100644
index 0000000000000..140646a0d1a35
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md
@@ -0,0 +1,16 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) > [cpuacct](./kibana-plugin-core-server.opsosmetrics.cpuacct.md)
+
+## OpsOsMetrics.cpuacct property
+
+cpu accounting metrics, undefined when not running in a cgroup
+
+Signature:
+
+```typescript
+cpuacct?: {
+ control_group: string;
+ usage_nanos: number;
+ };
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md
index 5fedb76a9c8d7..8938608531139 100644
--- a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md
+++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md
@@ -16,6 +16,8 @@ export interface OpsOsMetrics
| Property | Type | Description |
| --- | --- | --- |
+| [cpu](./kibana-plugin-core-server.opsosmetrics.cpu.md) | {
control_group: string;
cfs_period_micros: number;
cfs_quota_micros: number;
stat: {
number_of_elapsed_periods: number;
number_of_times_throttled: number;
time_throttled_nanos: number;
};
}
| cpu cgroup metrics, undefined when not running in a cgroup |
+| [cpuacct](./kibana-plugin-core-server.opsosmetrics.cpuacct.md) | {
control_group: string;
usage_nanos: number;
}
| cpu accounting metrics, undefined when not running in a cgroup |
| [distro](./kibana-plugin-core-server.opsosmetrics.distro.md) | string
| The os distrib. Only present for linux platforms |
| [distroRelease](./kibana-plugin-core-server.opsosmetrics.distrorelease.md) | string
| The os distrib release, prefixed by the os distrib. Only present for linux platforms |
| [load](./kibana-plugin-core-server.opsosmetrics.load.md) | {
'1m': number;
'5m': number;
'15m': number;
}
| cpu load metrics |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md
index 6ef7b991bb159..650459bfdb435 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md
@@ -16,8 +16,6 @@ export interface SavedObjectsServiceSetup
When plugins access the Saved Objects client, a new client is created using the factory provided to `setClientFactory` and wrapped by all wrappers registered through `addClientWrapper`.
-All the setup APIs will throw if called after the service has started, and therefor cannot be used from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated.
-
## Example 1
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md
index 57c9e04966c1b..54e01d3110a2d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md
@@ -14,10 +14,6 @@ See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdef
registerType: (type: SavedObjectsType) => void;
```
-## Remarks
-
-The type definition is an aggregation of the legacy savedObjects `schema`, `mappings` and `migration` concepts. This API is the single entry point to register saved object types in the new platform.
-
## Example
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md
index 337b4b3302cc3..d32e9a955f890 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md
@@ -9,7 +9,6 @@
```typescript
export declare function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: {
- esShardTimeout: number;
getConfig: GetConfigFn;
}): ISearchRequestParams;
```
@@ -19,7 +18,7 @@ export declare function getSearchParamsFromRequest(searchRequest: SearchRequest,
| Parameter | Type | Description |
| --- | --- | --- |
| searchRequest | SearchRequest
| |
-| dependencies | {
esShardTimeout: number;
getConfig: GetConfigFn;
}
| |
+| dependencies | {
getConfig: GetConfigFn;
}
| |
Returns:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md
index 2e078e3404fe6..a5bb15c963978 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md
@@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class
Signature:
```typescript
-constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps);
+constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps);
```
## Parameters
@@ -17,5 +17,5 @@ constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCach
| Parameter | Type | Description |
| --- | --- | --- |
| id | string | undefined
| |
-| { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps
| |
+| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
index 4c53af3f8970e..87ce1e258712a 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md
@@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern
class |
+| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern
class |
## Properties
@@ -29,11 +29,13 @@ export declare class IndexPattern implements IIndexPattern
| [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string
| |
| [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined
| |
| [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[]
| |
+| [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) | | {
[key: string]: any;
}
| |
| [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[]
| |
| [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined
| |
| [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string
| |
| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | undefined
| |
| [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta
| |
+| [version](./kibana-plugin-plugins-data-public.indexpattern.version.md) | | string | undefined
| |
## Methods
@@ -60,6 +62,5 @@ export declare class IndexPattern implements IIndexPattern
| [prepBody()](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) | | |
| [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | |
| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | |
-| [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | |
| [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md
new file mode 100644
index 0000000000000..4bc3c76afbae9
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md)
+
+## IndexPattern.originalBody property
+
+Signature:
+
+```typescript
+originalBody: {
+ [key: string]: any;
+ };
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md
index 42c6dd72b8c4e..e902d9c42b082 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md
@@ -7,7 +7,7 @@
Signature:
```typescript
-removeScriptedField(fieldName: string): Promise;
+removeScriptedField(fieldName: string): void;
```
## Parameters
@@ -18,5 +18,5 @@ removeScriptedField(fieldName: string): Promise;
Returns:
-`Promise`
+`void`
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md
deleted file mode 100644
index d0b471cc2bc21..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [save](./kibana-plugin-plugins-data-public.indexpattern.save.md)
-
-## IndexPattern.save() method
-
-Signature:
-
-```typescript
-save(saveAttempts?: number): Promise;
-```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| saveAttempts | number
| |
-
-Returns:
-
-`Promise`
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md
new file mode 100644
index 0000000000000..99d3bc4e7a04d
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [version](./kibana-plugin-plugins-data-public.indexpattern.version.md)
+
+## IndexPattern.version property
+
+Signature:
+
+```typescript
+version: string | undefined;
+```
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 b651480a85899..0c493ca492953 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
@@ -69,6 +69,7 @@
| [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | |
| [Query](./kibana-plugin-plugins-data-public.query.md) | |
| [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state |
+| [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | |
| [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* |
| [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* |
| [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md
new file mode 100644
index 0000000000000..b358e9477e515
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) > [appFilters](./kibana-plugin-plugins-data-public.querystatechange.appfilters.md)
+
+## QueryStateChange.appFilters property
+
+Signature:
+
+```typescript
+appFilters?: boolean;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md
new file mode 100644
index 0000000000000..c395f169c35a5
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) > [globalFilters](./kibana-plugin-plugins-data-public.querystatechange.globalfilters.md)
+
+## QueryStateChange.globalFilters property
+
+Signature:
+
+```typescript
+globalFilters?: boolean;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md
new file mode 100644
index 0000000000000..71fb211da11d2
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md)
+
+## QueryStateChange interface
+
+Signature:
+
+```typescript
+export interface QueryStateChange extends QueryStateChangePartial
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [appFilters](./kibana-plugin-plugins-data-public.querystatechange.appfilters.md) | boolean
| |
+| [globalFilters](./kibana-plugin-plugins-data-public.querystatechange.globalfilters.md) | boolean
| |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md
index 9f3ed8c1263ba..3dbfd9430e913 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-QueryStringInput: React.FC>
+QueryStringInput: React.FC>
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
index 498691c06285d..d1d20291a6799 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
@@ -7,7 +7,7 @@
Signature:
```typescript
-SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & {
- WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
+SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "timeHistory" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "onFiltersUpdated">, any> & {
+ WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
}
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md
index 6f5dd1076fb40..4c67639300883 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md
@@ -4,12 +4,12 @@
## SearchInterceptor.(constructor)
-This class should be instantiated with a `requestTimeout` corresponding with how many ms after requests are initiated that they should automatically cancel.
+Constructs a new instance of the `SearchInterceptor` class
Signature:
```typescript
-constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined);
+constructor(deps: SearchInterceptorDeps);
```
## Parameters
@@ -17,5 +17,4 @@ constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined);
| Parameter | Type | Description |
| --- | --- | --- |
| deps | SearchInterceptorDeps
| |
-| requestTimeout | number | undefined
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md
index 32954927504ae..fd9f23a7f0052 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md
@@ -14,21 +14,18 @@ export declare class SearchInterceptor
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)(deps, requestTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | This class should be instantiated with a requestTimeout
corresponding with how many ms after requests are initiated that they should automatically cancel. |
+| [(constructor)(deps)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | Constructs a new instance of the SearchInterceptor
class |
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps
| |
-| [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) | | number | undefined
| |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [getPendingCount$()](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | Returns an Observable
over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. |
-| [runSearch(request, signal, strategy)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | |
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search
method. Overrides the AbortSignal
with one that will abort either when cancelPending
is called, when the request times out, or when the original AbortSignal
is aborted. Updates pendingCount$
when the request is started/finalized. |
-| [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md
deleted file mode 100644
index 3123433762991..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md)
-
-## SearchInterceptor.requestTimeout property
-
-Signature:
-
-```typescript
-protected readonly requestTimeout?: number | undefined;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md
deleted file mode 100644
index ad1d1dcb59d7b..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [runSearch](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md)
-
-## SearchInterceptor.runSearch() method
-
-Signature:
-
-```typescript
-protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable;
-```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| request | IEsSearchRequest
| |
-| signal | AbortSignal
| |
-| strategy | string
| |
-
-Returns:
-
-`Observable`
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md
deleted file mode 100644
index fe35655258b4c..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [setupTimers](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md)
-
-## SearchInterceptor.setupTimers() method
-
-Signature:
-
-```typescript
-protected setupTimers(options?: ISearchOptions): {
- combinedSignal: AbortSignal;
- cleanup: () => void;
- };
-```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| options | ISearchOptions
| |
-
-Returns:
-
-`{
- combinedSignal: AbortSignal;
- cleanup: () => void;
- }`
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
index e515c3513df6c..6ed20beb396f1 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
@@ -20,6 +20,7 @@ UI_SETTINGS: {
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
+ readonly SEARCH_TIMEOUT: "search:timeout";
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
readonly HISTORY_LIMIT: "history:limit";
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md
index 9de005c1fd0dd..e718ca42ca30f 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md
@@ -7,24 +7,26 @@
Signature:
```typescript
-export declare function getDefaultSearchParams(config: SharedGlobalConfig): {
- timeout: string;
+export declare function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<{
+ maxConcurrentShardRequests: number | undefined;
+ ignoreThrottled: boolean;
ignoreUnavailable: boolean;
- restTotalHitsAsInt: boolean;
-};
+ trackTotalHits: boolean;
+}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| config | SharedGlobalConfig
| |
+| uiSettingsClient | IUiSettingsClient
| |
Returns:
-`{
- timeout: string;
+`Promise<{
+ maxConcurrentShardRequests: number | undefined;
+ ignoreThrottled: boolean;
ignoreUnavailable: boolean;
- restTotalHitsAsInt: boolean;
-}`
+ trackTotalHits: boolean;
+}>`
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md
new file mode 100644
index 0000000000000..d7e2a597ff33d
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md
@@ -0,0 +1,30 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [getShardTimeout](./kibana-plugin-plugins-data-server.getshardtimeout.md)
+
+## getShardTimeout() function
+
+Signature:
+
+```typescript
+export declare function getShardTimeout(config: SharedGlobalConfig): {
+ timeout: string;
+} | {
+ timeout?: undefined;
+};
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| config | SharedGlobalConfig
| |
+
+Returns:
+
+`{
+ timeout: string;
+} | {
+ timeout?: undefined;
+}`
+
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
index 70c32adeab9fd..f5b587d86b349 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
@@ -26,11 +26,13 @@
| Function | Description |
| --- | --- |
-| [getDefaultSearchParams(config)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | |
+| [getDefaultSearchParams(uiSettingsClient)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | |
+| [getShardTimeout(config)](./kibana-plugin-plugins-data-server.getshardtimeout.md) | |
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | |
| [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally |
| [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | |
+| [toSnakeCase(obj)](./kibana-plugin-plugins-data-server.tosnakecase.md) | |
| [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | |
## Interfaces
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md
index 2d9104ef894bc..455c5ecdd8195 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md
@@ -8,7 +8,7 @@
```typescript
start(core: CoreStart): {
- search: ISearchStart>;
+ search: ISearchStart>;
fieldFormats: {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise;
};
@@ -27,7 +27,7 @@ start(core: CoreStart): {
Returns:
`{
- search: ISearchStart>;
+ search: ISearchStart>;
fieldFormats: {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise;
};
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md
new file mode 100644
index 0000000000000..eda9e9c312e59
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [toSnakeCase](./kibana-plugin-plugins-data-server.tosnakecase.md)
+
+## toSnakeCase() function
+
+Signature:
+
+```typescript
+export declare function toSnakeCase(obj: Record): import("lodash").Dictionary;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| obj | Record<string, any>
| |
+
+Returns:
+
+`import("lodash").Dictionary`
+
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
index e419b64cd43aa..2d4ce75b956df 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
@@ -20,6 +20,7 @@ UI_SETTINGS: {
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
+ readonly SEARCH_TIMEOUT: "search:timeout";
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
readonly HISTORY_LIMIT: "history:limit";
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index a64a0330ae43f..ed20166c87f29 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -225,6 +225,7 @@ be inconsistent because different shards might be in different refresh states.
`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results.
Searching through frozen indices
might increase the search time. This setting is off by default. Users must opt-in to include frozen indices.
+`search:timeout`:: Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion.
[float]
[[kibana-siem-settings]]
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 4a931aabd3646..f03022e9e9f00 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -20,12 +20,12 @@ which may cause a delay before pages start being served.
Set to `false` to disable Console. *Default: `true`*
| `cpu.cgroup.path.override:`
- | Override for cgroup cpu path when mounted in a
-manner that is inconsistent with `/proc/self/cgroup`.
+ | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuPath`
+and the old name will no longer be supported as of 8.0.
| `cpuacct.cgroup.path.override:`
- | Override for cgroup cpuacct path when mounted
-in a manner that is inconsistent with `/proc/self/cgroup`.
+ | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuAcctPath`
+and the old name will no longer be supported as of 8.0.
| `csp.rules:`
| A https://w3c.github.io/webappsec-csp/[content-security-policy] template
@@ -438,6 +438,14 @@ not saved in {es}. *Default: `data`*
| Set the interval in milliseconds to sample
system and process performance metrics. The minimum value is 100. *Default: `5000`*
+| `ops.cGroupOverrides.cpuPath:`
+ | Override for cgroup cpu path when mounted in a
+manner that is inconsistent with `/proc/self/cgroup`.
+
+| `ops.cGroupOverrides.cpuAcctPath:`
+ | Override for cgroup cpuacct path when mounted
+in a manner that is inconsistent with `/proc/self/cgroup`.
+
| `server.basePath:`
| Enables you to specify a path to mount {kib} at if you are
running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib}
diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc
index 0c0151cc3ace2..d88a3eb5092df 100644
--- a/docs/user/dashboard/dashboard.asciidoc
+++ b/docs/user/dashboard/dashboard.asciidoc
@@ -4,9 +4,9 @@
[partintro]
--
-A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that
-you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations,
-interactive controls, markdown, and more.
+A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that
+you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations,
+interactive controls, markdown, and more.
With *Dashboard*s, you can:
@@ -18,7 +18,7 @@ With *Dashboard*s, you can:
* Create and apply filters to focus on the data you want to display.
-* Control who can use your data, and share the dashboard with a small or large audience.
+* Control who can use your data, and share the dashboard with a small or large audience.
* Generate reports based on your findings.
@@ -42,7 +42,7 @@ image::images/dashboard-read-only-badge.png[Example of Dashboard read only acces
[[types-of-panels]]
== Types of panels
-Panels contain everything you need to tell a story about you data, including visualizations,
+Panels contain everything you need to tell a story about you data, including visualizations,
interactive controls, Markdown, and more.
[cols="50, 50"]
@@ -50,30 +50,30 @@ interactive controls, Markdown, and more.
a| *Area*
-Displays data points, connected by a line, where the area between the line and axes are shaded.
+Displays data points, connected by a line, where the area between the line and axes are shaded.
Use area charts to compare two or more categories over time, and display the magnitude of trends.
| image:images/area.png[Area chart]
a| *Stacked area*
-Displays the evolution of the value of several data groups. The values of each group are displayed
-on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show
+Displays the evolution of the value of several data groups. The values of each group are displayed
+on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show
how each category contributes to the cumulative total.
| image:images/stacked_area.png[Stacked area chart]
a| *Bar*
-Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a
-large number of categories, display data that includes categories with negative values, and easily identify
+Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a
+large number of categories, display data that includes categories with negative values, and easily identify
the categories that represent the highest and lowest values. Kibana also supports horizontal bar charts.
| image:images/bar.png[Bar chart]
a| *Stacked bar*
-Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between
+Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between
levels of a categorical value. Kibana also supports stacked horizontal bar charts.
| image:images/stacked_bar.png[Stacked area chart]
@@ -81,15 +81,15 @@ levels of a categorical value. Kibana also supports stacked horizontal bar chart
a| *Line*
-Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover
+Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover
trends over time, and forecast future values.
| image:images/line.png[Line chart]
a| *Pie*
-Displays slices that represent a data category, where the slice size is proportional to the quantity it represents.
-Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others,
+Displays slices that represent a data category, where the slice size is proportional to the quantity it represents.
+Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others,
and show percentage or proportional data.
| image:images/pie.png[Pie chart]
@@ -103,7 +103,7 @@ Similar to the pie chart, but the central circle is removed. Use donut charts wh
a| *Tree map*
-Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on
+Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on
its proportion to the whole. Use treemaps to make efficient use of space to show percent total for each category.
| image:images/treemap.png[Tree map]
@@ -111,7 +111,7 @@ its proportion to the whole. Use treemaps to make efficient use of space to show
a| *Heat map*
-Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes
+Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes
categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data.
| image:images/heat_map.png[Heat map]
@@ -125,7 +125,7 @@ Displays how your metric progresses toward a fixed goal. Use the goal to display
a| *Gauge*
-Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric
+Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric
values relate to reference threshold values, or determine how a specified field is performing versus how it is expected to perform.
| image:images/gauge.png[Gauge]
@@ -133,7 +133,7 @@ values relate to reference threshold values, or determine how a specified field
a| *Metric*
-Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell
+Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell
a story about your data.
| image:images/metric.png[Metric]
@@ -141,7 +141,7 @@ a story about your data.
a| *Data table*
-Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min,
+Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min,
or max values for a specific field, and monitor the status of key services.
| image:images/data_table.png[Data table]
@@ -149,7 +149,7 @@ or max values for a specific field, and monitor the status of key services.
a| *Tag cloud*
-Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and
+Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and
create visual art for a specific topic.
| image:images/tag_cloud.png[Tag cloud]
@@ -168,16 +168,16 @@ For all your mapping needs, use <>.
[[create-panels]]
== Create panels
-To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <>
-to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information,
+To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <>
+to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information,
refer to <>.
-To begin, click *Create new*, then choose one of the following options on the
+To begin, click *Create new*, then choose one of the following options on the
*New Visualization* window:
-* Click on the type of panel you want to create, then configure the options.
+* Click on the type of panel you want to create, then configure the options.
-* Select an editor to help you create the panel.
+* Select an editor to help you create the panel.
[role="screenshot"]
image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard]
@@ -188,19 +188,19 @@ image:images/Dashboard_add_new_visualization.png[Example add new visualization t
[[lens]]
=== Create panels with Lens
-*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields
+*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields
as you want onto the visualization builder pane, and *Lens* uses heuristics to decide how to apply each field to the visualization.
With *Lens*, you can:
* Use the automatically generated suggestions to change the visualization type.
-* Create visualizations with multiple layers and indices.
+* Create visualizations with multiple layers and indices.
* Change the aggregation and labels to customize the data.
[role="screenshot"]
image::images/lens_drag_drop.gif[Drag and drop]
-TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a
+TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a
visualization, configure the customization options for your visualization.
[float]
@@ -220,7 +220,7 @@ To filter the data fields:
[[view-data-summaries]]
==== View data summaries
-To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of
+To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of
values within the specified time range.
To view the data field summary information, navigate to the field, then click *i*.
@@ -250,10 +250,10 @@ When there is an exclamation point (!) next to a visualization type, *Lens* is u
[[customize-the-data]]
==== Customize the data
-For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type.
+For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type.
. Click a data field name in the editor, or click *Drop a field here*.
-. Change the options that appear.
+. Change the options that appear.
+
[role="screenshot"]
image::images/lens_aggregation_labels.png[Quick function options]
@@ -262,7 +262,7 @@ image::images/lens_aggregation_labels.png[Quick function options]
[[add-layers-and-indices]]
==== Add layers and indices
-To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are
+To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are
supported in area, line, and bar charts.
To add a layer, click *+*, then drag and drop the data fields for the new layer.
@@ -281,7 +281,7 @@ Ready to try out *Lens*? Refer to the <>.
[[tsvb]]
=== Create panels with TSVB
-*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*,
+*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*,
you can combine an infinite number of <> to display your data.
With *TSVB*, you can:
@@ -295,15 +295,15 @@ image::images/tsvb.png[TSVB UI]
[float]
[[configure-the-data]]
-==== Configure the data
+==== Configure the data
-With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build
+With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build
complex summaries of that data.
. Select *Data*. If you are using *Table*, select *Columns*.
-. From the *Aggregation* drop down, select the aggregation you want to visualize.
+. From the *Aggregation* drop down, select the aggregation you want to visualize.
+
-If you don’t see any data, change the <>.
+If you don’t see any data, change the <>.
+
To add multiple aggregations, click *+*.
. From the *Group by* drop down, select how you want to group or split the data.
@@ -315,14 +315,14 @@ When you have more than one aggregation, the last value is displayed, which is i
[[change-the-data-display]]
==== Change the data display
-To find the best way to display your data, *TSVB* supports several types of panels and charts.
+To find the best way to display your data, *TSVB* supports several types of panels and charts.
To change the *Time Series* chart type:
. Click *Data > Options*.
. Select the *Chart type*.
-To change the panel type, click on the panel options:
+To change the panel type, click on the panel options:
[role="screenshot"]
image::images/tsvb_change_display.gif[TSVB change the panel type]
@@ -331,7 +331,7 @@ image::images/tsvb_change_display.gif[TSVB change the panel type]
[[custommize-the-data]]
==== Customize the data
-View data in a different <>, and change the data label name and colors. The options available depend on the panel type.
+View data in a different <>, and change the data label name and colors. The options available depend on the panel type.
To change the index pattern, click *Panel options*, then enter the new *Index Pattern*.
@@ -361,7 +361,7 @@ image::images/tsvb_annotations.png[TSVB annotations]
[[filter-the-panel]]
==== Filter the panel
-The data that displays on the panel is based on the <> and <>.
+The data that displays on the panel is based on the <> and <>.
You can filter the data on the panels using the <>.
Click *Panel options*, then enter the syntax in the *Panel Filter* field.
@@ -372,7 +372,7 @@ If you want to ignore filters from all of {kib}, select *Yes* for *Ignore global
[[vega]]
=== Create custom panels with Vega
-Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service,
+Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service,
URL, or static data. Use the {kib} extensions to embed *Vega* in your dashboard, and add interactive tools.
Use *Vega* and *Vega-Lite* when you want to create a visualization for:
@@ -405,7 +405,7 @@ For more information about *Vega* and *Vega-Lite*, refer to:
[[timelion]]
=== Create panels with Timelion
-*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization.
+*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization.
*Timelion* is driven by a simple expression language that you use to:
@@ -422,9 +422,41 @@ Ready to try out Timelion? For step-by-step tutorials, refer to:
* <>
* <>
+[float]
+[[timelion-deprecation]]
+==== Timelion app deprecation
+
+Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard.
+
+NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion
+visualizations on dashboards and in Visualize and Canvas.
+
+To migrate a Timelion worksheet to a dashboard:
+
+. Open the menu, click **Dashboard**, then click **Create dashboard**.
+
+. On the dashboard, click **Create New**, then select the Timelion visualization.
+
+. On a new tab, open the Timelion app, select the chart you want to copy, and copy its expression.
++
+[role="screenshot"]
+image::images/timelion-copy-expression.png[]
+
+. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**.
++
+[role="screenshot"]
+image::images/timelion-vis-paste-expression.png[]
+
+. Save the new visualization, give it a name, and click **Save and Return**.
++
+Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet.
++
+[role="screenshot"]
+image::images/timelion-dashboard.png[]
+
[float]
[[save-panels]]
-=== Save panels
+== Save panels
When you’ve finished making changes, save the panels.
@@ -436,7 +468,7 @@ When you’ve finished making changes, save the panels.
[[add-existing-panels]]
== Add existing panels
-Add panels that you’ve already created to your dashboard.
+Add panels that you’ve already created to your dashboard.
On the dashboard, click *Add an existing*, then select the panel you want to add.
@@ -445,7 +477,7 @@ When a panel contains a stored query, both queries are applied.
[role="screenshot"]
image:images/Dashboard_add_visualization.png[Example add visualization to dashboard]
-To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu.
+To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu.
The changes you make appear in every dashboard that uses the panel, except if you edit the panel title. Changes to the panel title appear only on the dashboard where you made the change.
[float]
diff --git a/kibana.d.ts b/kibana.d.ts
index d64752abd8b60..517bda374af9d 100644
--- a/kibana.d.ts
+++ b/kibana.d.ts
@@ -39,8 +39,6 @@ export namespace Legacy {
export type KibanaConfig = LegacyKibanaServer.KibanaConfig;
export type Request = LegacyKibanaServer.Request;
export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit;
- export type SavedObjectsClient = LegacyKibanaServer.SavedObjectsClient;
- export type SavedObjectsService = LegacyKibanaServer.SavedObjectsLegacyService;
export type Server = LegacyKibanaServer.Server;
export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction;
diff --git a/package.json b/package.json
index ff487510f7a32..95a6de337f62a 100644
--- a/package.json
+++ b/package.json
@@ -231,7 +231,7 @@
"@babel/parser": "^7.11.2",
"@babel/types": "^7.11.0",
"@elastic/apm-rum": "^5.5.0",
- "@elastic/charts": "21.0.1",
+ "@elastic/charts": "21.1.2",
"@elastic/ems-client": "7.9.3",
"@elastic/eslint-config-kibana": "0.15.0",
"@elastic/eslint-plugin-eui": "0.0.2",
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 4b2e88d155245..bbe7b1bc2e8da 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,7 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@elastic/charts": "21.0.1",
+ "@elastic/charts": "21.1.2",
"@elastic/eui": "28.2.0",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js
index c81da4689052a..fa80dfdeef20f 100644
--- a/packages/kbn-ui-shared-deps/webpack.config.js
+++ b/packages/kbn-ui-shared-deps/webpack.config.js
@@ -32,22 +32,10 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({
mode: dev ? 'development' : 'production',
entry: {
'kbn-ui-shared-deps': './entry.js',
- 'kbn-ui-shared-deps.v7.dark': [
- '@elastic/eui/dist/eui_theme_dark.css',
- '@elastic/charts/dist/theme_only_dark.css',
- ],
- 'kbn-ui-shared-deps.v7.light': [
- '@elastic/eui/dist/eui_theme_light.css',
- '@elastic/charts/dist/theme_only_light.css',
- ],
- 'kbn-ui-shared-deps.v8.dark': [
- '@elastic/eui/dist/eui_theme_amsterdam_dark.css',
- '@elastic/charts/dist/theme_only_dark.css',
- ],
- 'kbn-ui-shared-deps.v8.light': [
- '@elastic/eui/dist/eui_theme_amsterdam_light.css',
- '@elastic/charts/dist/theme_only_light.css',
- ],
+ 'kbn-ui-shared-deps.v7.dark': ['@elastic/eui/dist/eui_theme_dark.css'],
+ 'kbn-ui-shared-deps.v7.light': ['@elastic/eui/dist/eui_theme_light.css'],
+ 'kbn-ui-shared-deps.v8.dark': ['@elastic/eui/dist/eui_theme_amsterdam_dark.css'],
+ 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'],
},
context: __dirname,
devtool: dev ? '#cheap-source-map' : false,
diff --git a/src/legacy/utils/binder.ts b/src/cli/cluster/binder.ts
similarity index 100%
rename from src/legacy/utils/binder.ts
rename to src/cli/cluster/binder.ts
diff --git a/src/legacy/utils/binder_for.ts b/src/cli/cluster/binder_for.ts
similarity index 100%
rename from src/legacy/utils/binder_for.ts
rename to src/cli/cluster/binder_for.ts
diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts
index 097a549187429..c8a8a067d30bf 100644
--- a/src/cli/cluster/worker.ts
+++ b/src/cli/cluster/worker.ts
@@ -21,7 +21,7 @@ import _ from 'lodash';
import cluster from 'cluster';
import { EventEmitter } from 'events';
-import { BinderFor } from '../../legacy/utils/binder_for';
+import { BinderFor } from './binder_for';
import { fromRoot } from '../../core/server/utils';
const cliPath = fromRoot('src/cli');
diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js
index 462259ec942dd..232392f34c63b 100644
--- a/src/cli_keystore/add.js
+++ b/src/cli_keystore/add.js
@@ -18,7 +18,7 @@
*/
import { Logger } from '../cli_plugin/lib/logger';
-import { confirm, question } from '../legacy/server/utils';
+import { confirm, question } from './utils';
import { createPromiseFromStreams, createConcatStream } from '../core/server/utils';
/**
diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js
index b5d5009667eb4..f1adee8879bc2 100644
--- a/src/cli_keystore/add.test.js
+++ b/src/cli_keystore/add.test.js
@@ -42,7 +42,7 @@ import { PassThrough } from 'stream';
import { Keystore } from '../legacy/server/keystore';
import { add } from './add';
import { Logger } from '../cli_plugin/lib/logger';
-import * as prompt from '../legacy/server/utils/prompt';
+import * as prompt from './utils/prompt';
describe('Kibana keystore', () => {
describe('add', () => {
diff --git a/src/cli_keystore/create.js b/src/cli_keystore/create.js
index 8be1eb36882f1..55fe2c151dec0 100644
--- a/src/cli_keystore/create.js
+++ b/src/cli_keystore/create.js
@@ -18,7 +18,7 @@
*/
import { Logger } from '../cli_plugin/lib/logger';
-import { confirm } from '../legacy/server/utils';
+import { confirm } from './utils';
export async function create(keystore, command, options) {
const logger = new Logger(options);
diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js
index f48b3775ddfff..cb85475eab1cb 100644
--- a/src/cli_keystore/create.test.js
+++ b/src/cli_keystore/create.test.js
@@ -41,7 +41,7 @@ import sinon from 'sinon';
import { Keystore } from '../legacy/server/keystore';
import { create } from './create';
import { Logger } from '../cli_plugin/lib/logger';
-import * as prompt from '../legacy/server/utils/prompt';
+import * as prompt from './utils/prompt';
describe('Kibana keystore', () => {
describe('create', () => {
diff --git a/src/legacy/server/utils/index.js b/src/cli_keystore/utils/index.js
similarity index 100%
rename from src/legacy/server/utils/index.js
rename to src/cli_keystore/utils/index.js
diff --git a/src/legacy/server/utils/prompt.js b/src/cli_keystore/utils/prompt.js
similarity index 100%
rename from src/legacy/server/utils/prompt.js
rename to src/cli_keystore/utils/prompt.js
diff --git a/src/legacy/server/utils/prompt.test.js b/src/cli_keystore/utils/prompt.test.js
similarity index 100%
rename from src/legacy/server/utils/prompt.test.js
rename to src/cli_keystore/utils/prompt.test.js
diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts
index 3a444a4448467..5a9f982e106a7 100644
--- a/src/core/public/core_app/status/lib/load_status.test.ts
+++ b/src/core/public/core_app/status/lib/load_status.test.ts
@@ -57,6 +57,7 @@ const mockedResponse: StatusResponse = {
],
},
metrics: {
+ collected_at: new Date('2020-01-01 01:00:00'),
collection_interval_in_millis: 1000,
os: {
platform: 'darwin' as const,
diff --git a/src/core/public/core_app/styles/_globals_v7dark.scss b/src/core/public/core_app/styles/_globals_v7dark.scss
index 8ac841aab8469..9a4a965d63a38 100644
--- a/src/core/public/core_app/styles/_globals_v7dark.scss
+++ b/src/core/public/core_app/styles/_globals_v7dark.scss
@@ -3,9 +3,6 @@
// prepended to all .scss imports (from JS, when v7dark theme selected)
@import '@elastic/eui/src/themes/eui/eui_colors_dark';
-
-@import '@elastic/eui/src/global_styling/functions/index';
-@import '@elastic/eui/src/global_styling/variables/index';
-@import '@elastic/eui/src/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui/eui_globals';
@import './mixins';
diff --git a/src/core/public/core_app/styles/_globals_v7light.scss b/src/core/public/core_app/styles/_globals_v7light.scss
index 701bbdfe03662..ddb4b5b31fa1f 100644
--- a/src/core/public/core_app/styles/_globals_v7light.scss
+++ b/src/core/public/core_app/styles/_globals_v7light.scss
@@ -3,9 +3,6 @@
// prepended to all .scss imports (from JS, when v7light theme selected)
@import '@elastic/eui/src/themes/eui/eui_colors_light';
-
-@import '@elastic/eui/src/global_styling/functions/index';
-@import '@elastic/eui/src/global_styling/variables/index';
-@import '@elastic/eui/src/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui/eui_globals';
@import './mixins';
diff --git a/src/core/public/core_app/styles/_globals_v8dark.scss b/src/core/public/core_app/styles/_globals_v8dark.scss
index 972365e9e9d0e..9ad9108f350ff 100644
--- a/src/core/public/core_app/styles/_globals_v8dark.scss
+++ b/src/core/public/core_app/styles/_globals_v8dark.scss
@@ -3,14 +3,6 @@
// prepended to all .scss imports (from JS, when v8dark theme selected)
@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_dark';
-
-@import '@elastic/eui/src/global_styling/functions/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
-
-@import '@elastic/eui/src/global_styling/variables/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
-
-@import '@elastic/eui/src/global_styling/mixins/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_globals';
@import './mixins';
diff --git a/src/core/public/core_app/styles/_globals_v8light.scss b/src/core/public/core_app/styles/_globals_v8light.scss
index dc99f4d45082e..a6b2cb84c2062 100644
--- a/src/core/public/core_app/styles/_globals_v8light.scss
+++ b/src/core/public/core_app/styles/_globals_v8light.scss
@@ -3,14 +3,6 @@
// prepended to all .scss imports (from JS, when v8light theme selected)
@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light';
-
-@import '@elastic/eui/src/global_styling/functions/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
-
-@import '@elastic/eui/src/global_styling/variables/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
-
-@import '@elastic/eui/src/global_styling/mixins/index';
-@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_globals';
@import './mixins';
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index fc753517fd940..95ac8bba57049 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -129,7 +129,7 @@ export class DocLinksService {
},
visualize: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`,
- timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html#timelion-deprecation`,
+ timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`,
},
},
});
diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss
index 9b06b526fc7dd..427c6b7735435 100644
--- a/src/core/public/styles/_base.scss
+++ b/src/core/public/styles/_base.scss
@@ -1,4 +1,10 @@
+// Charts themes available app-wide
+@import '@elastic/charts/dist/theme';
+@import '@elastic/eui/src/themes/charts/theme';
+
+// Grab some nav-specific EUI vars
@import '@elastic/eui/src/components/collapsible_nav/variables';
+
// Application Layout
// chrome-context
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index e4e881ab24372..2b8b8e383da24 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -113,7 +113,7 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath,
return settings;
};
-export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [
+export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [
unusedFromRoot('savedObjects.indexCheckTimeout'),
unusedFromRoot('server.xsrf.token'),
unusedFromRoot('maps.manifestServiceUrl'),
@@ -136,6 +136,8 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromR
unusedFromRoot('optimize.workers'),
unusedFromRoot('optimize.profile'),
unusedFromRoot('optimize.validateSyntaxOfNodeModules'),
+ rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'),
+ rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'),
configPathDeprecation,
dataPathDeprecation,
rewriteBasePathDeprecation,
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index c17d3d7546779..97aca74bfd48f 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -266,9 +266,7 @@ export {
SavedObjectUnsanitizedDoc,
SavedObjectsRepositoryFactory,
SavedObjectsResolveImportErrorsOptions,
- SavedObjectsSchema,
SavedObjectsSerializer,
- SavedObjectsLegacyService,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts
index 26ec52185a5d8..c27f5be04d965 100644
--- a/src/core/server/legacy/legacy_service.mock.ts
+++ b/src/core/server/legacy/legacy_service.mock.ts
@@ -24,13 +24,7 @@ type LegacyServiceMock = jest.Mocked & { legacyId
const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({
pluginSpecs: [],
- uiExports: {
- savedObjectSchemas: {},
- savedObjectMappings: [],
- savedObjectMigrations: {},
- savedObjectValidations: {},
- savedObjectsManagement: {},
- },
+ uiExports: {},
navLinks: [],
pluginExtendedConfig: {
get: jest.fn(),
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index 880011d2e1923..ba3eb28f90c5c 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -264,6 +264,7 @@ export class LegacyService implements CoreService {
getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry,
},
metrics: {
+ collectionInterval: startDeps.core.metrics.collectionInterval,
getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$,
},
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
@@ -341,11 +342,9 @@ export class LegacyService implements CoreService {
registerStaticDir: setupDeps.core.http.registerStaticDir,
},
hapiServer: setupDeps.core.http.server,
- kibanaMigrator: startDeps.core.savedObjects.migrator,
uiPlugins: setupDeps.uiPlugins,
elasticsearch: setupDeps.core.elasticsearch,
rendering: setupDeps.core.rendering,
- savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider,
legacy: this.legacyInternals,
},
logger: this.coreContext.logger,
diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts
index cf08689a6d0d4..1105308fd44cf 100644
--- a/src/core/server/legacy/types.ts
+++ b/src/core/server/legacy/types.ts
@@ -24,7 +24,6 @@ import { KibanaRequest, LegacyRequest } from '../http';
import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins';
import { InternalRenderingServiceSetup } from '../rendering';
-import { SavedObjectsLegacyUiExports } from '../types';
/**
* @internal
@@ -128,13 +127,13 @@ export type LegacyNavLink = Omit;
unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }];
-};
+}
/**
* @public
diff --git a/src/core/server/metrics/collectors/cgroup.test.ts b/src/core/server/metrics/collectors/cgroup.test.ts
new file mode 100644
index 0000000000000..39f917b9f0ba1
--- /dev/null
+++ b/src/core/server/metrics/collectors/cgroup.test.ts
@@ -0,0 +1,115 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import mockFs from 'mock-fs';
+import { OsCgroupMetricsCollector } from './cgroup';
+
+describe('OsCgroupMetricsCollector', () => {
+ afterEach(() => mockFs.restore());
+
+ it('returns empty object when no cgroup file present', async () => {
+ mockFs({
+ '/proc/self': {
+ /** empty directory */
+ },
+ });
+
+ const collector = new OsCgroupMetricsCollector({});
+ expect(await collector.collect()).toEqual({});
+ });
+
+ it('collects default cgroup data', async () => {
+ mockFs({
+ '/proc/self/cgroup': `
+123:memory:/groupname
+123:cpu:/groupname
+123:cpuacct:/groupname
+ `,
+ '/sys/fs/cgroup/cpuacct/groupname/cpuacct.usage': '111',
+ '/sys/fs/cgroup/cpu/groupname/cpu.cfs_period_us': '222',
+ '/sys/fs/cgroup/cpu/groupname/cpu.cfs_quota_us': '333',
+ '/sys/fs/cgroup/cpu/groupname/cpu.stat': `
+nr_periods 444
+nr_throttled 555
+throttled_time 666
+ `,
+ });
+
+ const collector = new OsCgroupMetricsCollector({});
+ expect(await collector.collect()).toMatchInlineSnapshot(`
+ Object {
+ "cpu": Object {
+ "cfs_period_micros": 222,
+ "cfs_quota_micros": 333,
+ "control_group": "/groupname",
+ "stat": Object {
+ "number_of_elapsed_periods": 444,
+ "number_of_times_throttled": 555,
+ "time_throttled_nanos": 666,
+ },
+ },
+ "cpuacct": Object {
+ "control_group": "/groupname",
+ "usage_nanos": 111,
+ },
+ }
+ `);
+ });
+
+ it('collects override cgroup data', async () => {
+ mockFs({
+ '/proc/self/cgroup': `
+123:memory:/groupname
+123:cpu:/groupname
+123:cpuacct:/groupname
+ `,
+ '/sys/fs/cgroup/cpuacct/xxcustomcpuacctxx/cpuacct.usage': '111',
+ '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.cfs_period_us': '222',
+ '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.cfs_quota_us': '333',
+ '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.stat': `
+nr_periods 444
+nr_throttled 555
+throttled_time 666
+ `,
+ });
+
+ const collector = new OsCgroupMetricsCollector({
+ cpuAcctPath: 'xxcustomcpuacctxx',
+ cpuPath: 'xxcustomcpuxx',
+ });
+ expect(await collector.collect()).toMatchInlineSnapshot(`
+ Object {
+ "cpu": Object {
+ "cfs_period_micros": 222,
+ "cfs_quota_micros": 333,
+ "control_group": "xxcustomcpuxx",
+ "stat": Object {
+ "number_of_elapsed_periods": 444,
+ "number_of_times_throttled": 555,
+ "time_throttled_nanos": 666,
+ },
+ },
+ "cpuacct": Object {
+ "control_group": "xxcustomcpuacctxx",
+ "usage_nanos": 111,
+ },
+ }
+ `);
+ });
+});
diff --git a/src/core/server/metrics/collectors/cgroup.ts b/src/core/server/metrics/collectors/cgroup.ts
new file mode 100644
index 0000000000000..867ea44dff1ae
--- /dev/null
+++ b/src/core/server/metrics/collectors/cgroup.ts
@@ -0,0 +1,194 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import fs from 'fs';
+import { join as joinPath } from 'path';
+import { MetricsCollector, OpsOsMetrics } from './types';
+
+type OsCgroupMetrics = Pick;
+
+interface OsCgroupMetricsCollectorOptions {
+ cpuPath?: string;
+ cpuAcctPath?: string;
+}
+
+export class OsCgroupMetricsCollector implements MetricsCollector {
+ /** Used to prevent unnecessary file reads on systems not using cgroups. */
+ private noCgroupPresent = false;
+ private cpuPath?: string;
+ private cpuAcctPath?: string;
+
+ constructor(private readonly options: OsCgroupMetricsCollectorOptions) {}
+
+ public async collect(): Promise {
+ try {
+ await this.initializePaths();
+ if (this.noCgroupPresent || !this.cpuAcctPath || !this.cpuPath) {
+ return {};
+ }
+
+ const [cpuAcctUsage, cpuFsPeriod, cpuFsQuota, cpuStat] = await Promise.all([
+ readCPUAcctUsage(this.cpuAcctPath),
+ readCPUFsPeriod(this.cpuPath),
+ readCPUFsQuota(this.cpuPath),
+ readCPUStat(this.cpuPath),
+ ]);
+
+ return {
+ cpuacct: {
+ control_group: this.cpuAcctPath,
+ usage_nanos: cpuAcctUsage,
+ },
+
+ cpu: {
+ control_group: this.cpuPath,
+ cfs_period_micros: cpuFsPeriod,
+ cfs_quota_micros: cpuFsQuota,
+ stat: cpuStat,
+ },
+ };
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ this.noCgroupPresent = true;
+ return {};
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ public reset() {}
+
+ private async initializePaths() {
+ // Perform this setup lazily on the first collect call and then memoize the results.
+ // Makes the assumption this data doesn't change while the process is running.
+ if (this.cpuPath && this.cpuAcctPath) {
+ return;
+ }
+
+ // Only read the file if both options are undefined.
+ if (!this.options.cpuPath || !this.options.cpuAcctPath) {
+ const cgroups = await readControlGroups();
+ this.cpuPath = this.options.cpuPath || cgroups[GROUP_CPU];
+ this.cpuAcctPath = this.options.cpuAcctPath || cgroups[GROUP_CPUACCT];
+ } else {
+ this.cpuPath = this.options.cpuPath;
+ this.cpuAcctPath = this.options.cpuAcctPath;
+ }
+
+ // prevents undefined cgroup paths
+ if (!this.cpuPath || !this.cpuAcctPath) {
+ this.noCgroupPresent = true;
+ }
+ }
+}
+
+const CONTROL_GROUP_RE = new RegExp('\\d+:([^:]+):(/.*)');
+const CONTROLLER_SEPARATOR_RE = ',';
+
+const PROC_SELF_CGROUP_FILE = '/proc/self/cgroup';
+const PROC_CGROUP_CPU_DIR = '/sys/fs/cgroup/cpu';
+const PROC_CGROUP_CPUACCT_DIR = '/sys/fs/cgroup/cpuacct';
+
+const GROUP_CPUACCT = 'cpuacct';
+const CPUACCT_USAGE_FILE = 'cpuacct.usage';
+
+const GROUP_CPU = 'cpu';
+const CPU_FS_PERIOD_US_FILE = 'cpu.cfs_period_us';
+const CPU_FS_QUOTA_US_FILE = 'cpu.cfs_quota_us';
+const CPU_STATS_FILE = 'cpu.stat';
+
+async function readControlGroups() {
+ const data = await fs.promises.readFile(PROC_SELF_CGROUP_FILE);
+
+ return data
+ .toString()
+ .split(/\n/)
+ .reduce((acc, line) => {
+ const matches = line.match(CONTROL_GROUP_RE);
+
+ if (matches !== null) {
+ const controllers = matches[1].split(CONTROLLER_SEPARATOR_RE);
+ controllers.forEach((controller) => {
+ acc[controller] = matches[2];
+ });
+ }
+
+ return acc;
+ }, {} as Record);
+}
+
+async function fileContentsToInteger(path: string) {
+ const data = await fs.promises.readFile(path);
+ return parseInt(data.toString(), 10);
+}
+
+function readCPUAcctUsage(controlGroup: string) {
+ return fileContentsToInteger(joinPath(PROC_CGROUP_CPUACCT_DIR, controlGroup, CPUACCT_USAGE_FILE));
+}
+
+function readCPUFsPeriod(controlGroup: string) {
+ return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_PERIOD_US_FILE));
+}
+
+function readCPUFsQuota(controlGroup: string) {
+ return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_QUOTA_US_FILE));
+}
+
+async function readCPUStat(controlGroup: string) {
+ const stat = {
+ number_of_elapsed_periods: -1,
+ number_of_times_throttled: -1,
+ time_throttled_nanos: -1,
+ };
+
+ try {
+ const data = await fs.promises.readFile(
+ joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_STATS_FILE)
+ );
+ return data
+ .toString()
+ .split(/\n/)
+ .reduce((acc, line) => {
+ const fields = line.split(/\s+/);
+
+ switch (fields[0]) {
+ case 'nr_periods':
+ acc.number_of_elapsed_periods = parseInt(fields[1], 10);
+ break;
+
+ case 'nr_throttled':
+ acc.number_of_times_throttled = parseInt(fields[1], 10);
+ break;
+
+ case 'throttled_time':
+ acc.time_throttled_nanos = parseInt(fields[1], 10);
+ break;
+ }
+
+ return acc;
+ }, stat);
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ return stat;
+ }
+
+ throw err;
+ }
+}
diff --git a/src/core/server/metrics/collectors/collector.mock.ts b/src/core/server/metrics/collectors/collector.mock.ts
new file mode 100644
index 0000000000000..2a942e1fafe63
--- /dev/null
+++ b/src/core/server/metrics/collectors/collector.mock.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { MetricsCollector } from './types';
+
+const createCollector = (collectReturnValue: any = {}): jest.Mocked> => {
+ const collector: jest.Mocked> = {
+ collect: jest.fn().mockResolvedValue(collectReturnValue),
+ reset: jest.fn(),
+ };
+
+ return collector;
+};
+
+export const metricsCollectorMock = {
+ create: createCollector,
+};
diff --git a/src/core/server/metrics/collectors/index.ts b/src/core/server/metrics/collectors/index.ts
index f58ab02e63881..4540cb79be74b 100644
--- a/src/core/server/metrics/collectors/index.ts
+++ b/src/core/server/metrics/collectors/index.ts
@@ -18,6 +18,6 @@
*/
export { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics, MetricsCollector } from './types';
-export { OsMetricsCollector } from './os';
+export { OsMetricsCollector, OpsMetricsCollectorOptions } from './os';
export { ProcessMetricsCollector } from './process';
export { ServerMetricsCollector } from './server';
diff --git a/src/core/server/saved_objects/schema/index.ts b/src/core/server/metrics/collectors/os.test.mocks.ts
similarity index 77%
rename from src/core/server/saved_objects/schema/index.ts
rename to src/core/server/metrics/collectors/os.test.mocks.ts
index d30bbb8d34cd3..ee02b8c802151 100644
--- a/src/core/server/saved_objects/schema/index.ts
+++ b/src/core/server/metrics/collectors/os.test.mocks.ts
@@ -17,4 +17,9 @@
* under the License.
*/
-export { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema';
+import { metricsCollectorMock } from './collector.mock';
+
+export const cgroupCollectorMock = metricsCollectorMock.create();
+jest.doMock('./cgroup', () => ({
+ OsCgroupMetricsCollector: jest.fn(() => cgroupCollectorMock),
+}));
diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts
index 7d5a6da90b7d6..5e52cecb76be3 100644
--- a/src/core/server/metrics/collectors/os.test.ts
+++ b/src/core/server/metrics/collectors/os.test.ts
@@ -20,6 +20,7 @@
jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' }));
import os from 'os';
+import { cgroupCollectorMock } from './os.test.mocks';
import { OsMetricsCollector } from './os';
describe('OsMetricsCollector', () => {
@@ -27,6 +28,8 @@ describe('OsMetricsCollector', () => {
beforeEach(() => {
collector = new OsMetricsCollector();
+ cgroupCollectorMock.collect.mockReset();
+ cgroupCollectorMock.reset.mockReset();
});
afterEach(() => {
@@ -96,4 +99,9 @@ describe('OsMetricsCollector', () => {
'15m': fifteenMinLoad,
});
});
+
+ it('calls the cgroup sub-collector', async () => {
+ await collector.collect();
+ expect(cgroupCollectorMock.collect).toHaveBeenCalled();
+ });
});
diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts
index 59bef9d8ddd2b..eae49278405a9 100644
--- a/src/core/server/metrics/collectors/os.ts
+++ b/src/core/server/metrics/collectors/os.ts
@@ -21,10 +21,22 @@ import os from 'os';
import getosAsync, { LinuxOs } from 'getos';
import { promisify } from 'util';
import { OpsOsMetrics, MetricsCollector } from './types';
+import { OsCgroupMetricsCollector } from './cgroup';
const getos = promisify(getosAsync);
+export interface OpsMetricsCollectorOptions {
+ cpuPath?: string;
+ cpuAcctPath?: string;
+}
+
export class OsMetricsCollector implements MetricsCollector {
+ private readonly cgroupCollector: OsCgroupMetricsCollector;
+
+ constructor(options: OpsMetricsCollectorOptions = {}) {
+ this.cgroupCollector = new OsCgroupMetricsCollector(options);
+ }
+
public async collect(): Promise {
const platform = os.platform();
const load = os.loadavg();
@@ -43,20 +55,30 @@ export class OsMetricsCollector implements MetricsCollector {
used_in_bytes: os.totalmem() - os.freemem(),
},
uptime_in_millis: os.uptime() * 1000,
+ ...(await this.getDistroStats(platform)),
+ ...(await this.cgroupCollector.collect()),
};
+ return metrics;
+ }
+
+ public reset() {}
+
+ private async getDistroStats(
+ platform: string
+ ): Promise> {
if (platform === 'linux') {
try {
const distro = (await getos()) as LinuxOs;
- metrics.distro = distro.dist;
- metrics.distroRelease = `${distro.dist}-${distro.release}`;
+ return {
+ distro: distro.dist,
+ distroRelease: `${distro.dist}-${distro.release}`,
+ };
} catch (e) {
// ignore errors
}
}
- return metrics;
+ return {};
}
-
- public reset() {}
}
diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts
index 73e8975a6b362..77ea13a1f0787 100644
--- a/src/core/server/metrics/collectors/types.ts
+++ b/src/core/server/metrics/collectors/types.ts
@@ -85,6 +85,33 @@ export interface OpsOsMetrics {
};
/** the OS uptime */
uptime_in_millis: number;
+
+ /** cpu accounting metrics, undefined when not running in a cgroup */
+ cpuacct?: {
+ /** name of this process's cgroup */
+ control_group: string;
+ /** cpu time used by this process's cgroup */
+ usage_nanos: number;
+ };
+
+ /** cpu cgroup metrics, undefined when not running in a cgroup */
+ cpu?: {
+ /** name of this process's cgroup */
+ control_group: string;
+ /** the length of the cfs period */
+ cfs_period_micros: number;
+ /** total available run-time within a cfs period */
+ cfs_quota_micros: number;
+ /** current stats on the cfs periods */
+ stat: {
+ /** number of cfs periods that elapsed */
+ number_of_elapsed_periods: number;
+ /** number of times the cgroup has been throttled */
+ number_of_times_throttled: number;
+ /** total amount of time the cgroup has been throttled for */
+ time_throttled_nanos: number;
+ };
+ };
}
/**
diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts
index 769f6ee2a549a..2af653004a479 100644
--- a/src/core/server/metrics/metrics_service.mock.ts
+++ b/src/core/server/metrics/metrics_service.mock.ts
@@ -21,20 +21,18 @@ import { MetricsService } from './metrics_service';
import {
InternalMetricsServiceSetup,
InternalMetricsServiceStart,
+ MetricsServiceSetup,
MetricsServiceStart,
} from './types';
const createInternalSetupContractMock = () => {
- const setupContract: jest.Mocked = {};
- return setupContract;
-};
-
-const createStartContractMock = () => {
- const startContract: jest.Mocked = {
+ const setupContract: jest.Mocked = {
+ collectionInterval: 30000,
getOpsMetrics$: jest.fn(),
};
- startContract.getOpsMetrics$.mockReturnValue(
+ setupContract.getOpsMetrics$.mockReturnValue(
new BehaviorSubject({
+ collected_at: new Date('2020-01-01 01:00:00'),
process: {
memory: {
heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 },
@@ -56,11 +54,21 @@ const createStartContractMock = () => {
concurrent_connections: 1,
})
);
+ return setupContract;
+};
+
+const createSetupContractMock = () => {
+ const startContract: jest.Mocked = createInternalSetupContractMock();
return startContract;
};
const createInternalStartContractMock = () => {
- const startContract: jest.Mocked = createStartContractMock();
+ const startContract: jest.Mocked = createInternalSetupContractMock();
+ return startContract;
+};
+
+const createStartContractMock = () => {
+ const startContract: jest.Mocked = createInternalSetupContractMock();
return startContract;
};
@@ -77,7 +85,7 @@ const createMock = () => {
export const metricsServiceMock = {
create: createMock,
- createSetupContract: createStartContractMock,
+ createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
createInternalSetupContract: createInternalSetupContractMock,
createInternalStartContract: createInternalStartContractMock,
diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts
index f28fb21aaac0d..d4696b3aa9aaf 100644
--- a/src/core/server/metrics/metrics_service.ts
+++ b/src/core/server/metrics/metrics_service.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Subject } from 'rxjs';
+import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
@@ -37,26 +37,21 @@ export class MetricsService
private readonly logger: Logger;
private metricsCollector?: OpsMetricsCollector;
private collectInterval?: NodeJS.Timeout;
- private metrics$ = new Subject();
+ private metrics$ = new ReplaySubject();
+ private service?: InternalMetricsServiceSetup;
constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get('metrics');
}
public async setup({ http }: MetricsServiceSetupDeps): Promise {
- this.metricsCollector = new OpsMetricsCollector(http.server);
- return {};
- }
-
- public async start(): Promise {
- if (!this.metricsCollector) {
- throw new Error('#setup() needs to be run first');
- }
const config = await this.coreContext.configService
.atPath(opsConfig.path)
.pipe(first())
.toPromise();
+ this.metricsCollector = new OpsMetricsCollector(http.server, config.cGroupOverrides);
+
await this.refreshMetrics();
this.collectInterval = setInterval(() => {
@@ -65,9 +60,20 @@ export class MetricsService
const metricsObservable = this.metrics$.asObservable();
- return {
+ this.service = {
+ collectionInterval: config.interval.asMilliseconds(),
getOpsMetrics$: () => metricsObservable,
};
+
+ return this.service;
+ }
+
+ public async start(): Promise {
+ if (!this.service) {
+ throw new Error('#setup() needs to be run first');
+ }
+
+ return this.service;
}
private async refreshMetrics() {
diff --git a/src/core/server/metrics/ops_config.ts b/src/core/server/metrics/ops_config.ts
index bd6ae5cc5474d..5f3f67e931c38 100644
--- a/src/core/server/metrics/ops_config.ts
+++ b/src/core/server/metrics/ops_config.ts
@@ -23,6 +23,10 @@ export const opsConfig = {
path: 'ops',
schema: schema.object({
interval: schema.duration({ defaultValue: '5s' }),
+ cGroupOverrides: schema.object({
+ cpuPath: schema.maybe(schema.string()),
+ cpuAcctPath: schema.maybe(schema.string()),
+ }),
}),
};
diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts
index 9e76895b14578..7aa3f7cd3baf0 100644
--- a/src/core/server/metrics/ops_metrics_collector.test.ts
+++ b/src/core/server/metrics/ops_metrics_collector.test.ts
@@ -30,7 +30,7 @@ describe('OpsMetricsCollector', () => {
beforeEach(() => {
const hapiServer = httpServiceMock.createInternalSetupContract().server;
- collector = new OpsMetricsCollector(hapiServer);
+ collector = new OpsMetricsCollector(hapiServer, {});
mockOsCollector.collect.mockResolvedValue('osMetrics');
});
@@ -51,6 +51,7 @@ describe('OpsMetricsCollector', () => {
expect(mockServerCollector.collect).toHaveBeenCalledTimes(1);
expect(metrics).toEqual({
+ collected_at: expect.any(Date),
process: 'processMetrics',
os: 'osMetrics',
requests: 'serverRequestsMetrics',
diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts
index 525515dba1457..af74caa6cb386 100644
--- a/src/core/server/metrics/ops_metrics_collector.ts
+++ b/src/core/server/metrics/ops_metrics_collector.ts
@@ -21,6 +21,7 @@ import { Server as HapiServer } from 'hapi';
import {
ProcessMetricsCollector,
OsMetricsCollector,
+ OpsMetricsCollectorOptions,
ServerMetricsCollector,
MetricsCollector,
} from './collectors';
@@ -31,9 +32,9 @@ export class OpsMetricsCollector implements MetricsCollector {
private readonly osCollector: OsMetricsCollector;
private readonly serverCollector: ServerMetricsCollector;
- constructor(server: HapiServer) {
+ constructor(server: HapiServer, opsOptions: OpsMetricsCollectorOptions) {
this.processCollector = new ProcessMetricsCollector();
- this.osCollector = new OsMetricsCollector();
+ this.osCollector = new OsMetricsCollector(opsOptions);
this.serverCollector = new ServerMetricsCollector(server);
}
@@ -44,6 +45,7 @@ export class OpsMetricsCollector implements MetricsCollector {
this.serverCollector.collect(),
]);
return {
+ collected_at: new Date(),
process,
os,
...server,
diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts
index cbf0acacd6bab..c177b3ed25115 100644
--- a/src/core/server/metrics/types.ts
+++ b/src/core/server/metrics/types.ts
@@ -20,14 +20,15 @@
import { Observable } from 'rxjs';
import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors';
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface MetricsServiceSetup {}
/**
* APIs to retrieves metrics gathered and exposed by the core platform.
*
* @public
*/
-export interface MetricsServiceStart {
+export interface MetricsServiceSetup {
+ /** Interval metrics are collected in milliseconds */
+ readonly collectionInterval: number;
+
/**
* Retrieve an observable emitting the {@link OpsMetrics} gathered.
* The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time,
@@ -42,6 +43,12 @@ export interface MetricsServiceStart {
*/
getOpsMetrics$: () => Observable;
}
+/**
+ * {@inheritdoc MetricsServiceSetup}
+ *
+ * @public
+ */
+export type MetricsServiceStart = MetricsServiceSetup;
export type InternalMetricsServiceSetup = MetricsServiceSetup;
export type InternalMetricsServiceStart = MetricsServiceStart;
@@ -53,6 +60,8 @@ export type InternalMetricsServiceStart = MetricsServiceStart;
* @public
*/
export interface OpsMetrics {
+ /** Time metrics were recorded at. */
+ collected_at: Date;
/** Process related metrics */
process: OpsProcessMetrics;
/** OS related metrics */
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index fa2659ca130a0..5c389855d9ea2 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -233,6 +233,7 @@ export function createPluginStartContext(
getTypeRegistry: deps.savedObjects.getTypeRegistry,
},
metrics: {
+ collectionInterval: deps.metrics.collectionInterval,
getOpsMetrics$: deps.metrics.getOpsMetrics$,
},
uiSettings: {
diff --git a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap
deleted file mode 100644
index 7cd0297e57857..0000000000000
--- a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap
+++ /dev/null
@@ -1,184 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`convertLegacyTypes converts the legacy mappings using default values if no schemas are specified 1`] = `
-Array [
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldA": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeA",
- "namespaceType": "single",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldB": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeB",
- "namespaceType": "single",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldC": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeC",
- "namespaceType": "single",
- },
-]
-`;
-
-exports[`convertLegacyTypes merges everything when all are present 1`] = `
-Array [
- Object {
- "convertToAliasScript": undefined,
- "hidden": true,
- "indexPattern": "myIndex",
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldA": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {
- "1.0.0": [Function],
- "2.0.4": [Function],
- },
- "name": "typeA",
- "namespaceType": "agnostic",
- },
- Object {
- "convertToAliasScript": "some alias script",
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "anotherFieldB": Object {
- "type": "boolean",
- },
- "fieldB": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeB",
- "namespaceType": "single",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldC": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {
- "1.5.3": [Function],
- },
- "name": "typeC",
- "namespaceType": "single",
- },
-]
-`;
-
-exports[`convertLegacyTypes merges the mappings and the schema to create the type when schema exists for the type 1`] = `
-Array [
- Object {
- "convertToAliasScript": undefined,
- "hidden": true,
- "indexPattern": "fooBar",
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldA": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeA",
- "namespaceType": "agnostic",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": "barBaz",
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldB": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeB",
- "namespaceType": "multiple",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": undefined,
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldC": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeC",
- "namespaceType": "single",
- },
- Object {
- "convertToAliasScript": undefined,
- "hidden": false,
- "indexPattern": "bazQux",
- "management": undefined,
- "mappings": Object {
- "properties": Object {
- "fieldD": Object {
- "type": "text",
- },
- },
- },
- "migrations": Object {},
- "name": "typeD",
- "namespaceType": "agnostic",
- },
-]
-`;
diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts
index a294b28753f7b..f2bae29c4743b 100644
--- a/src/core/server/saved_objects/index.ts
+++ b/src/core/server/saved_objects/index.ts
@@ -19,8 +19,6 @@
export * from './service';
-export { SavedObjectsSchema } from './schema';
-
export * from './import';
export {
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index 4fc94d1992869..4cc4f696d307c 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -48,7 +48,6 @@ describe('DocumentMigrator', () => {
return {
kibanaVersion: '25.2.3',
typeRegistry: createRegistry(),
- validateDoc: _.noop,
log: mockLogger,
};
}
@@ -60,7 +59,6 @@ describe('DocumentMigrator', () => {
name: 'foo',
migrations: _.noop as any,
}),
- validateDoc: _.noop,
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
@@ -77,7 +75,6 @@ describe('DocumentMigrator', () => {
bar: (doc) => doc,
},
}),
- validateDoc: _.noop,
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
@@ -94,7 +91,6 @@ describe('DocumentMigrator', () => {
'1.2.3': 23 as any,
},
}),
- validateDoc: _.noop,
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
@@ -633,27 +629,6 @@ describe('DocumentMigrator', () => {
bbb: '3.2.3',
});
});
-
- test('fails if the validate doc throws', () => {
- const migrator = new DocumentMigrator({
- ...testOpts(),
- typeRegistry: createRegistry({
- name: 'aaa',
- migrations: {
- '2.3.4': (d) => set(d, 'attributes.counter', 42),
- },
- }),
- validateDoc: (d) => {
- if ((d.attributes as any).counter === 42) {
- throw new Error('Meaningful!');
- }
- },
- });
-
- const doc = { id: '1', type: 'foo', attributes: {}, migrationVersion: {}, aaa: {} };
-
- expect(() => migrator.migrate(doc)).toThrow(/Meaningful/);
- });
});
function renameAttr(path: string, newPath: string) {
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts
index c50f755fda994..345704fbfd783 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts
@@ -73,12 +73,9 @@ import { SavedObjectMigrationFn } from '../types';
export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
-type ValidateDoc = (doc: SavedObjectUnsanitizedDoc) => void;
-
interface DocumentMigratorOptions {
kibanaVersion: string;
typeRegistry: ISavedObjectTypeRegistry;
- validateDoc: ValidateDoc;
log: Logger;
}
@@ -113,19 +110,16 @@ export class DocumentMigrator implements VersionedTransformer {
* @param {DocumentMigratorOptions} opts
* @prop {string} kibanaVersion - The current version of Kibana
* @prop {SavedObjectTypeRegistry} typeRegistry - The type registry to get type migrations from
- * @prop {ValidateDoc} validateDoc - A function which, given a document throws an error if it is
- * not up to date. This is used to ensure we don't let unmigrated documents slip through.
* @prop {Logger} log - The migration logger
* @memberof DocumentMigrator
*/
- constructor({ typeRegistry, kibanaVersion, log, validateDoc }: DocumentMigratorOptions) {
+ constructor({ typeRegistry, kibanaVersion, log }: DocumentMigratorOptions) {
validateMigrationDefinition(typeRegistry);
this.migrations = buildActiveMigrations(typeRegistry, log);
this.transformDoc = buildDocumentTransform({
kibanaVersion,
migrations: this.migrations,
- validateDoc,
});
}
@@ -231,21 +225,16 @@ function buildActiveMigrations(
* Creates a function which migrates and validates any document that is passed to it.
*/
function buildDocumentTransform({
- kibanaVersion,
migrations,
- validateDoc,
}: {
kibanaVersion: string;
migrations: ActiveMigrations;
- validateDoc: ValidateDoc;
}): TransformFn {
return function transformAndValidate(doc: SavedObjectUnsanitizedDoc) {
const result = doc.migrationVersion
? applyMigrations(doc, migrations)
: markAsUpToDate(doc, migrations);
- validateDoc(result);
-
// In order to keep tests a bit more stable, we won't
// tack on an empy migrationVersion to docs that have
// no migrations defined.
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index df89137a1d798..13f771c16bc67 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -369,6 +369,30 @@ describe('IndexMigrator', () => {
],
});
});
+
+ test('rejects when the migration function throws an error', async () => {
+ const { client } = testOpts;
+ const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
+ throw new Error('error migrating document');
+ });
+
+ testOpts.documentMigrator = {
+ migrationVersion: { foo: '1.2.3' },
+ migrate: migrateDoc,
+ };
+
+ withIndex(client, {
+ numOutOfDate: 1,
+ docs: [
+ [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }],
+ [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }],
+ ],
+ });
+
+ await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"error migrating document"`
+ );
+ });
});
function withIndex(
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
index 4c9d2e870a7bb..83dc042d2b96b 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
@@ -90,4 +90,18 @@ describe('migrateRawDocs', () => {
expect(logger.error).toBeCalledTimes(1);
});
+
+ test('rejects when the transform function throws an error', async () => {
+ const transform = jest.fn((doc: any) => {
+ throw new Error('error during transform');
+ });
+ await expect(
+ migrateRawDocs(
+ new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
+ transform,
+ [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }],
+ createSavedObjectsMigrationLoggerMock()
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`);
+ });
});
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
index 2bdf59d25dc74..5a5048d8ad88f 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
@@ -78,10 +78,14 @@ function transformNonBlocking(
): (doc: SavedObjectUnsanitizedDoc) => Promise {
// promises aren't enough to unblock the event loop
return (doc: SavedObjectUnsanitizedDoc) =>
- new Promise((resolve) => {
+ new Promise((resolve, reject) => {
// set immediate is though
setImmediate(() => {
- resolve(transform(doc));
+ try {
+ resolve(transform(doc));
+ } catch (e) {
+ reject(e);
+ }
});
});
}
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
index cc443093e30a3..7eb2cfefe4620 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
@@ -134,7 +134,6 @@ const mockOptions = () => {
const options: MockedOptions = {
logger: loggingSystemMock.create().get(),
kibanaVersion: '8.2.3',
- savedObjectValidations: {},
typeRegistry: createRegistry([
{
name: 'testtype',
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
index 85b9099308807..b9f24a75c01d2 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
@@ -28,7 +28,6 @@ import { BehaviorSubject } from 'rxjs';
import { Logger } from '../../../logging';
import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings';
import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization';
-import { docValidator, PropertyValidators } from '../../validation';
import { buildActiveMappings, IndexMigrator, MigrationResult, MigrationStatus } from '../core';
import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator';
import { MigrationEsClient } from '../core/';
@@ -44,7 +43,6 @@ export interface KibanaMigratorOptions {
kibanaConfig: KibanaConfigType;
kibanaVersion: string;
logger: Logger;
- savedObjectValidations: PropertyValidators;
}
export type IKibanaMigrator = Pick;
@@ -80,7 +78,6 @@ export class KibanaMigrator {
typeRegistry,
kibanaConfig,
savedObjectsConfig,
- savedObjectValidations,
kibanaVersion,
logger,
}: KibanaMigratorOptions) {
@@ -94,7 +91,6 @@ export class KibanaMigrator {
this.documentMigrator = new DocumentMigrator({
kibanaVersion,
typeRegistry,
- validateDoc: docValidator(savedObjectValidations || {}),
log: this.log,
});
// Building the active mappings (and associated md5sums) is an expensive
diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts
index 6f5ecb1eb464b..e3d44c20dd190 100644
--- a/src/core/server/saved_objects/saved_objects_service.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_service.mock.ts
@@ -26,8 +26,7 @@ import {
SavedObjectsServiceSetup,
SavedObjectsServiceStart,
} from './saved_objects_service';
-import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock';
-import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock';
+
import { savedObjectsRepositoryMock } from './service/lib/repository.mock';
import { savedObjectsClientMock } from './service/saved_objects_client.mock';
import { typeRegistryMock } from './saved_objects_type_registry.mock';
@@ -54,11 +53,7 @@ const createStartContractMock = () => {
};
const createInternalStartContractMock = () => {
- const internalStartContract: jest.Mocked = {
- ...createStartContractMock(),
- clientProvider: savedObjectsClientProviderMock.create(),
- migrator: mockKibanaMigrator.create(),
- };
+ const internalStartContract: jest.Mocked = createStartContractMock();
return internalStartContract;
};
diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts
index 8df6a07318c45..d6b30889eba5f 100644
--- a/src/core/server/saved_objects/saved_objects_service.test.ts
+++ b/src/core/server/saved_objects/saved_objects_service.test.ts
@@ -33,7 +33,6 @@ import { Env } from '../config';
import { configServiceMock } from '../mocks';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { elasticsearchClientMock } from '../elasticsearch/client/mocks';
-import { legacyServiceMock } from '../legacy/legacy_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { httpServerMock } from '../http/http_server.mocks';
import { SavedObjectsClientFactoryProvider } from './service/lib';
@@ -65,7 +64,6 @@ describe('SavedObjectsService', () => {
return {
http: httpServiceMock.createInternalSetupContract(),
elasticsearch: elasticsearchMock,
- legacyPlugins: legacyServiceMock.createDiscoverPlugins(),
};
};
@@ -239,8 +237,7 @@ describe('SavedObjectsService', () => {
await soService.setup(createSetupDeps());
expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0);
- const startContract = await soService.start(createStartDeps());
- expect(startContract.migrator).toBe(migratorInstanceMock);
+ await soService.start(createStartDeps());
expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1);
});
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index f05e912b12ad8..5cc59d55a254e 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -23,12 +23,10 @@ import { CoreService } from '../../types';
import {
SavedObjectsClient,
SavedObjectsClientProvider,
- ISavedObjectsClientProvider,
SavedObjectsClientProviderOptions,
} from './';
import { KibanaMigrator, IKibanaMigrator } from './migrations';
import { CoreContext } from '../core_context';
-import { LegacyServiceDiscoverPlugins } from '../legacy';
import {
ElasticsearchClient,
IClusterClient,
@@ -49,9 +47,7 @@ import {
SavedObjectsClientWrapperFactory,
} from './service/lib/scoped_client_provider';
import { Logger } from '../logging';
-import { convertLegacyTypes } from './utils';
import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry';
-import { PropertyValidators } from './validation';
import { SavedObjectsSerializer } from './serialization';
import { registerRoutes } from './routes';
import { ServiceStatus } from '../status';
@@ -67,9 +63,6 @@ import { createMigrationEsClient } from './migrations/core/';
* the factory provided to `setClientFactory` and wrapped by all wrappers
* registered through `addClientWrapper`.
*
- * All the setup APIs will throw if called after the service has started, and therefor cannot be used
- * from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated.
- *
* @example
* ```ts
* import { SavedObjectsClient, CoreSetup } from 'src/core/server';
@@ -155,9 +148,6 @@ export interface SavedObjectsServiceSetup {
* }
* }
* ```
- *
- * @remarks The type definition is an aggregation of the legacy savedObjects `schema`, `mappings` and `migration` concepts.
- * This API is the single entry point to register saved object types in the new platform.
*/
registerType: (type: SavedObjectsType) => void;
@@ -230,16 +220,7 @@ export interface SavedObjectsServiceStart {
getTypeRegistry: () => ISavedObjectTypeRegistry;
}
-export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceStart {
- /**
- * @deprecated Exposed only for injecting into Legacy
- */
- migrator: IKibanaMigrator;
- /**
- * @deprecated Exposed only for injecting into Legacy
- */
- clientProvider: ISavedObjectsClientProvider;
-}
+export type InternalSavedObjectsServiceStart = SavedObjectsServiceStart;
/**
* Factory provided when invoking a {@link SavedObjectsClientFactoryProvider | client factory provider}
@@ -271,7 +252,6 @@ export interface SavedObjectsRepositoryFactory {
/** @internal */
export interface SavedObjectsSetupDeps {
http: InternalHttpServiceSetup;
- legacyPlugins: LegacyServiceDiscoverPlugins;
elasticsearch: InternalElasticsearchServiceSetup;
}
@@ -296,9 +276,8 @@ export class SavedObjectsService
private clientFactoryProvider?: SavedObjectsClientFactoryProvider;
private clientFactoryWrappers: WrappedClientFactoryWrapper[] = [];
- private migrator$ = new Subject();
+ private migrator$ = new Subject();
private typeRegistry = new SavedObjectTypeRegistry();
- private validations: PropertyValidators = {};
private started = false;
constructor(private readonly coreContext: CoreContext) {
@@ -310,13 +289,6 @@ export class SavedObjectsService
this.setupDeps = setupDeps;
- const legacyTypes = convertLegacyTypes(
- setupDeps.legacyPlugins.uiExports,
- setupDeps.legacyPlugins.pluginExtendedConfig
- );
- legacyTypes.forEach((type) => this.typeRegistry.registerType(type));
- this.validations = setupDeps.legacyPlugins.uiExports.savedObjectValidations || {};
-
const savedObjectsConfig = await this.coreContext.configService
.atPath('savedObjects')
.pipe(first())
@@ -471,8 +443,6 @@ export class SavedObjectsService
this.started = true;
return {
- migrator,
- clientProvider,
getScopedClient: clientProvider.getClient.bind(clientProvider),
createScopedRepository: repositoryFactory.createScopedRepository,
createInternalRepository: repositoryFactory.createInternalRepository,
@@ -488,13 +458,12 @@ export class SavedObjectsService
savedObjectsConfig: SavedObjectsMigrationConfigType,
client: IClusterClient,
migrationsRetryDelay?: number
- ): KibanaMigrator {
+ ): IKibanaMigrator {
return new KibanaMigrator({
typeRegistry: this.typeRegistry,
logger: this.logger,
kibanaVersion: this.coreContext.env.packageInfo.version,
savedObjectsConfig,
- savedObjectValidations: this.validations,
kibanaConfig,
client: createMigrationEsClient(client.asInternalUser, this.logger, migrationsRetryDelay),
});
diff --git a/src/core/server/saved_objects/schema/schema.test.ts b/src/core/server/saved_objects/schema/schema.test.ts
deleted file mode 100644
index f2daa13e43fce..0000000000000
--- a/src/core/server/saved_objects/schema/schema.test.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema';
-
-describe('#isNamespaceAgnostic', () => {
- const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
- const schema = new SavedObjectsSchema(schemaDefinition);
- const result = schema.isNamespaceAgnostic('foo');
- expect(result).toBe(expected);
- };
-
- it(`returns false when no schema is defined`, () => {
- expectResult(false);
- });
-
- it(`returns false for unknown types`, () => {
- expectResult(false, { bar: {} });
- });
-
- it(`returns false for non-namespace-agnostic type`, () => {
- expectResult(false, { foo: { isNamespaceAgnostic: false } });
- expectResult(false, { foo: { isNamespaceAgnostic: undefined } });
- });
-
- it(`returns true for explicitly namespace-agnostic type`, () => {
- expectResult(true, { foo: { isNamespaceAgnostic: true } });
- });
-});
-
-describe('#isSingleNamespace', () => {
- const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
- const schema = new SavedObjectsSchema(schemaDefinition);
- const result = schema.isSingleNamespace('foo');
- expect(result).toBe(expected);
- };
-
- it(`returns true when no schema is defined`, () => {
- expectResult(true);
- });
-
- it(`returns true for unknown types`, () => {
- expectResult(true, { bar: {} });
- });
-
- it(`returns false for explicitly namespace-agnostic type`, () => {
- expectResult(false, { foo: { isNamespaceAgnostic: true } });
- });
-
- it(`returns false for explicitly multi-namespace type`, () => {
- expectResult(false, { foo: { multiNamespace: true } });
- });
-
- it(`returns true for non-namespace-agnostic and non-multi-namespace type`, () => {
- expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: false } });
- expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: undefined } });
- expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: false } });
- expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: undefined } });
- });
-});
-
-describe('#isMultiNamespace', () => {
- const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
- const schema = new SavedObjectsSchema(schemaDefinition);
- const result = schema.isMultiNamespace('foo');
- expect(result).toBe(expected);
- };
-
- it(`returns false when no schema is defined`, () => {
- expectResult(false);
- });
-
- it(`returns false for unknown types`, () => {
- expectResult(false, { bar: {} });
- });
-
- it(`returns false for explicitly namespace-agnostic type`, () => {
- expectResult(false, { foo: { isNamespaceAgnostic: true } });
- });
-
- it(`returns false for non-multi-namespace type`, () => {
- expectResult(false, { foo: { multiNamespace: false } });
- expectResult(false, { foo: { multiNamespace: undefined } });
- });
-
- it(`returns true for non-namespace-agnostic and explicitly multi-namespace type`, () => {
- expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: true } });
- expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: true } });
- });
-});
diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts
deleted file mode 100644
index ba1905158e822..0000000000000
--- a/src/core/server/saved_objects/schema/schema.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { LegacyConfig } from '../../legacy';
-
-/**
- * @deprecated
- * @internal
- **/
-interface SavedObjectsSchemaTypeDefinition {
- isNamespaceAgnostic?: boolean;
- multiNamespace?: boolean;
- hidden?: boolean;
- indexPattern?: ((config: LegacyConfig) => string) | string;
- convertToAliasScript?: string;
-}
-
-/**
- * @deprecated
- * @internal
- **/
-export interface SavedObjectsSchemaDefinition {
- [type: string]: SavedObjectsSchemaTypeDefinition;
-}
-
-/**
- * @deprecated This is only used by the {@link SavedObjectsLegacyService | legacy savedObjects service}
- * @internal
- **/
-export class SavedObjectsSchema {
- private readonly definition?: SavedObjectsSchemaDefinition;
- constructor(schemaDefinition?: SavedObjectsSchemaDefinition) {
- this.definition = schemaDefinition;
- }
-
- public isHiddenType(type: string) {
- if (this.definition && this.definition.hasOwnProperty(type)) {
- return Boolean(this.definition[type].hidden);
- }
-
- return false;
- }
-
- public getIndexForType(config: LegacyConfig, type: string): string | undefined {
- if (this.definition != null && this.definition.hasOwnProperty(type)) {
- const { indexPattern } = this.definition[type];
- return typeof indexPattern === 'function' ? indexPattern(config) : indexPattern;
- } else {
- return undefined;
- }
- }
-
- public getConvertToAliasScript(type: string): string | undefined {
- if (this.definition != null && this.definition.hasOwnProperty(type)) {
- return this.definition[type].convertToAliasScript;
- }
- }
-
- public isNamespaceAgnostic(type: string) {
- // if no plugins have registered a Saved Objects Schema,
- // this.schema will be undefined, and no types are namespace agnostic
- if (!this.definition) {
- return false;
- }
-
- const typeSchema = this.definition[type];
- if (!typeSchema) {
- return false;
- }
- return Boolean(typeSchema.isNamespaceAgnostic);
- }
-
- public isSingleNamespace(type: string) {
- // if no plugins have registered a Saved Objects Schema,
- // this.schema will be undefined, and all types are namespace isolated
- if (!this.definition) {
- return true;
- }
-
- const typeSchema = this.definition[type];
- if (!typeSchema) {
- return true;
- }
- return !Boolean(typeSchema.isNamespaceAgnostic) && !Boolean(typeSchema.multiNamespace);
- }
-
- public isMultiNamespace(type: string) {
- // if no plugins have registered a Saved Objects Schema,
- // this.schema will be undefined, and no types are multi-namespace
- if (!this.definition) {
- return false;
- }
-
- const typeSchema = this.definition[type];
- if (!typeSchema) {
- return false;
- }
- return !Boolean(typeSchema.isNamespaceAgnostic) && Boolean(typeSchema.multiNamespace);
- }
-}
diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts
index 9f625b4732e26..271d4dd67d43e 100644
--- a/src/core/server/saved_objects/service/index.ts
+++ b/src/core/server/saved_objects/service/index.ts
@@ -17,37 +17,6 @@
* under the License.
*/
-import { Readable } from 'stream';
-import { SavedObjectsClientProvider } from './lib';
-import { SavedObjectsClient } from './saved_objects_client';
-import { SavedObjectsExportOptions } from '../export';
-import { SavedObjectsImportOptions, SavedObjectsImportResponse } from '../import';
-import { SavedObjectsSchema } from '../schema';
-import { SavedObjectsResolveImportErrorsOptions } from '../import/types';
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacyService {
- // ATTENTION: these types are incomplete
- addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider['addClientWrapperFactory'];
- setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory'];
- getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
- SavedObjectsClient: typeof SavedObjectsClient;
- types: string[];
- schema: SavedObjectsSchema;
- getSavedObjectsRepository(...rest: any[]): any;
- importExport: {
- objectLimit: number;
- importSavedObjects(options: SavedObjectsImportOptions): Promise;
- resolveImportErrors(
- options: SavedObjectsResolveImportErrorsOptions
- ): Promise;
- getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise;
- };
-}
-
export {
SavedObjectsRepository,
SavedObjectsClientProvider,
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index b1d6028465713..f2e3b3e633cd6 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -153,7 +153,6 @@ describe('SavedObjectsRepository', () => {
typeRegistry: registry,
kibanaVersion: '2.0.0',
log: {},
- validateDoc: jest.fn(),
});
const getMockGetResponse = ({ type, id, references, namespace, originId }) => ({
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index dd25989725f3e..e3fb7d2306469 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -31,7 +31,7 @@ import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { SavedObjectsErrorHelpers, DecoratedError } from './errors';
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
-import { KibanaMigrator } from '../../migrations';
+import { IKibanaMigrator } from '../../migrations';
import {
SavedObjectsSerializer,
SavedObjectSanitizedDoc,
@@ -85,7 +85,7 @@ export interface SavedObjectsRepositoryOptions {
client: ElasticsearchClient;
typeRegistry: SavedObjectTypeRegistry;
serializer: SavedObjectsSerializer;
- migrator: KibanaMigrator;
+ migrator: IKibanaMigrator;
allowedTypes: string[];
}
@@ -120,7 +120,7 @@ export type ISavedObjectsRepository = Pick) => { path: string; uiCapabilitiesPath: string };
}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacyUiExports {
- savedObjectMappings: SavedObjectsLegacyMapping[];
- savedObjectMigrations: SavedObjectsLegacyMigrationDefinitions;
- savedObjectSchemas: SavedObjectsLegacySchemaDefinitions;
- savedObjectValidations: PropertyValidators;
- savedObjectsManagement: SavedObjectsLegacyManagementDefinition;
-}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacyMapping {
- pluginId: string;
- properties: SavedObjectsTypeMappingDefinitions;
-}
-
-/**
- * @internal
- * @deprecated Use {@link SavedObjectsTypeManagementDefinition | management definition} when registering
- * from new platform plugins
- */
-export interface SavedObjectsLegacyManagementDefinition {
- [key: string]: SavedObjectsLegacyManagementTypeDefinition;
-}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacyManagementTypeDefinition {
- isImportableAndExportable?: boolean;
- defaultSearchField?: string;
- icon?: string;
- getTitle?: (savedObject: SavedObject) => string;
- getEditUrl?: (savedObject: SavedObject) => string;
- getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string };
-}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacyMigrationDefinitions {
- [type: string]: SavedObjectLegacyMigrationMap;
-}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectLegacyMigrationMap {
- [version: string]: SavedObjectLegacyMigrationFn;
-}
-
-/**
- * @internal
- * @deprecated
- */
-export type SavedObjectLegacyMigrationFn = (
- doc: SavedObjectUnsanitizedDoc,
- log: SavedObjectsMigrationLogger
-) => SavedObjectUnsanitizedDoc;
-
-/**
- * @internal
- * @deprecated
- */
-interface SavedObjectsLegacyTypeSchema {
- isNamespaceAgnostic?: boolean;
- /** Cannot be used in conjunction with `isNamespaceAgnostic` */
- multiNamespace?: boolean;
- hidden?: boolean;
- indexPattern?: ((config: LegacyConfig) => string) | string;
- convertToAliasScript?: string;
-}
-
-/**
- * @internal
- * @deprecated
- */
-export interface SavedObjectsLegacySchemaDefinitions {
- [type: string]: SavedObjectsLegacyTypeSchema;
-}
diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts
deleted file mode 100644
index 21229bee489c2..0000000000000
--- a/src/core/server/saved_objects/utils.test.ts
+++ /dev/null
@@ -1,445 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { legacyServiceMock } from '../legacy/legacy_service.mock';
-import { convertLegacyTypes, convertTypesToLegacySchema } from './utils';
-import { SavedObjectsLegacyUiExports, SavedObjectsType } from './types';
-import { LegacyConfig, SavedObjectMigrationContext } from 'kibana/server';
-import { SavedObjectUnsanitizedDoc } from './serialization';
-
-describe('convertLegacyTypes', () => {
- let legacyConfig: ReturnType;
-
- beforeEach(() => {
- legacyConfig = legacyServiceMock.createLegacyConfig();
- });
-
- it('converts the legacy mappings using default values if no schemas are specified', () => {
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- typeB: {
- properties: {
- fieldB: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginB',
- properties: {
- typeC: {
- properties: {
- fieldC: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {},
- savedObjectSchemas: {},
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(converted).toMatchSnapshot();
- });
-
- it('merges the mappings and the schema to create the type when schema exists for the type', () => {
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginB',
- properties: {
- typeB: {
- properties: {
- fieldB: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginC',
- properties: {
- typeC: {
- properties: {
- fieldC: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginD',
- properties: {
- typeD: {
- properties: {
- fieldD: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {},
- savedObjectSchemas: {
- typeA: {
- indexPattern: 'fooBar',
- hidden: true,
- isNamespaceAgnostic: true,
- },
- typeB: {
- indexPattern: 'barBaz',
- hidden: false,
- multiNamespace: true,
- },
- typeD: {
- indexPattern: 'bazQux',
- hidden: false,
- // if both isNamespaceAgnostic and multiNamespace are true, the resulting namespaceType is 'agnostic'
- isNamespaceAgnostic: true,
- multiNamespace: true,
- },
- },
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(converted).toMatchSnapshot();
- });
-
- it('invokes indexPattern to retrieve the index when it is a function', () => {
- const indexPatternAccessor: (config: LegacyConfig) => string = jest.fn((config) => {
- config.get('foo.bar');
- return 'myIndex';
- });
-
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {},
- savedObjectSchemas: {
- typeA: {
- indexPattern: indexPatternAccessor,
- hidden: true,
- isNamespaceAgnostic: true,
- },
- },
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
-
- expect(indexPatternAccessor).toHaveBeenCalledWith(legacyConfig);
- expect(legacyConfig.get).toHaveBeenCalledWith('foo.bar');
- expect(converted.length).toEqual(1);
- expect(converted[0].indexPattern).toEqual('myIndex');
- });
-
- it('import migrations from the uiExports', () => {
- const migrationsA = {
- '1.0.0': jest.fn(),
- '2.0.4': jest.fn(),
- };
- const migrationsB = {
- '1.5.3': jest.fn(),
- };
-
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginB',
- properties: {
- typeB: {
- properties: {
- fieldC: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {
- typeA: migrationsA,
- typeB: migrationsB,
- },
- savedObjectSchemas: {},
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(converted.length).toEqual(2);
- expect(Object.keys(converted[0]!.migrations!)).toEqual(Object.keys(migrationsA));
- expect(Object.keys(converted[1]!.migrations!)).toEqual(Object.keys(migrationsB));
- });
-
- it('converts the migration to the new format', () => {
- const legacyMigration = jest.fn();
- const migrationsA = {
- '1.0.0': legacyMigration,
- };
-
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {
- typeA: migrationsA,
- },
- savedObjectSchemas: {},
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(Object.keys(converted[0]!.migrations!)).toEqual(['1.0.0']);
-
- const migration = converted[0]!.migrations!['1.0.0']!;
-
- const doc = {} as SavedObjectUnsanitizedDoc;
- const context = { log: {} } as SavedObjectMigrationContext;
- migration(doc, context);
-
- expect(legacyMigration).toHaveBeenCalledTimes(1);
- expect(legacyMigration).toHaveBeenCalledWith(doc, context.log);
- });
-
- it('imports type management information', () => {
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- },
- },
- {
- pluginId: 'pluginB',
- properties: {
- typeB: {
- properties: {
- fieldB: { type: 'text' },
- },
- },
- typeC: {
- properties: {
- fieldC: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectsManagement: {
- typeA: {
- isImportableAndExportable: true,
- icon: 'iconA',
- defaultSearchField: 'searchFieldA',
- getTitle: (savedObject) => savedObject.id,
- },
- typeB: {
- isImportableAndExportable: false,
- icon: 'iconB',
- getEditUrl: (savedObject) => `/some-url/${savedObject.id}`,
- getInAppUrl: (savedObject) => ({ path: 'path', uiCapabilitiesPath: 'ui-path' }),
- },
- },
- savedObjectMigrations: {},
- savedObjectSchemas: {},
- savedObjectValidations: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(converted.length).toEqual(3);
- const [typeA, typeB, typeC] = converted;
-
- expect(typeA.management).toEqual({
- importableAndExportable: true,
- icon: 'iconA',
- defaultSearchField: 'searchFieldA',
- getTitle: uiExports.savedObjectsManagement.typeA.getTitle,
- });
-
- expect(typeB.management).toEqual({
- importableAndExportable: false,
- icon: 'iconB',
- getEditUrl: uiExports.savedObjectsManagement.typeB.getEditUrl,
- getInAppUrl: uiExports.savedObjectsManagement.typeB.getInAppUrl,
- });
-
- expect(typeC.management).toBeUndefined();
- });
-
- it('merges everything when all are present', () => {
- const uiExports: SavedObjectsLegacyUiExports = {
- savedObjectMappings: [
- {
- pluginId: 'pluginA',
- properties: {
- typeA: {
- properties: {
- fieldA: { type: 'text' },
- },
- },
- typeB: {
- properties: {
- fieldB: { type: 'text' },
- anotherFieldB: { type: 'boolean' },
- },
- },
- },
- },
- {
- pluginId: 'pluginB',
- properties: {
- typeC: {
- properties: {
- fieldC: { type: 'text' },
- },
- },
- },
- },
- ],
- savedObjectMigrations: {
- typeA: {
- '1.0.0': jest.fn(),
- '2.0.4': jest.fn(),
- },
- typeC: {
- '1.5.3': jest.fn(),
- },
- },
- savedObjectSchemas: {
- typeA: {
- indexPattern: jest.fn((config) => {
- config.get('foo.bar');
- return 'myIndex';
- }),
- hidden: true,
- isNamespaceAgnostic: true,
- },
- typeB: {
- convertToAliasScript: 'some alias script',
- hidden: false,
- },
- },
- savedObjectValidations: {},
- savedObjectsManagement: {},
- };
-
- const converted = convertLegacyTypes(uiExports, legacyConfig);
- expect(converted).toMatchSnapshot();
- });
-});
-
-describe('convertTypesToLegacySchema', () => {
- it('converts types to the legacy schema format', () => {
- const types: SavedObjectsType[] = [
- {
- name: 'typeA',
- hidden: false,
- namespaceType: 'agnostic',
- mappings: { properties: {} },
- convertToAliasScript: 'some script',
- },
- {
- name: 'typeB',
- hidden: true,
- namespaceType: 'single',
- indexPattern: 'myIndex',
- mappings: { properties: {} },
- },
- {
- name: 'typeC',
- hidden: false,
- namespaceType: 'multiple',
- mappings: { properties: {} },
- },
- ];
- expect(convertTypesToLegacySchema(types)).toEqual({
- typeA: {
- hidden: false,
- isNamespaceAgnostic: true,
- multiNamespace: false,
- convertToAliasScript: 'some script',
- },
- typeB: {
- hidden: true,
- isNamespaceAgnostic: false,
- multiNamespace: false,
- indexPattern: 'myIndex',
- },
- typeC: {
- hidden: false,
- isNamespaceAgnostic: false,
- multiNamespace: true,
- },
- });
- });
-});
diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts
deleted file mode 100644
index af7c08d1fbfcc..0000000000000
--- a/src/core/server/saved_objects/utils.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { LegacyConfig } from '../legacy';
-import { SavedObjectMigrationMap } from './migrations';
-import {
- SavedObjectsNamespaceType,
- SavedObjectsType,
- SavedObjectsLegacyUiExports,
- SavedObjectLegacyMigrationMap,
- SavedObjectsLegacyManagementTypeDefinition,
- SavedObjectsTypeManagementDefinition,
-} from './types';
-import { SavedObjectsSchemaDefinition } from './schema';
-
-/**
- * Converts the legacy savedObjects mappings, schema, and migrations
- * to actual {@link SavedObjectsType | saved object types}
- */
-export const convertLegacyTypes = (
- {
- savedObjectMappings = [],
- savedObjectMigrations = {},
- savedObjectSchemas = {},
- savedObjectsManagement = {},
- }: SavedObjectsLegacyUiExports,
- legacyConfig: LegacyConfig
-): SavedObjectsType[] => {
- return savedObjectMappings.reduce((types, { properties }) => {
- return [
- ...types,
- ...Object.entries(properties).map(([type, mappings]) => {
- const schema = savedObjectSchemas[type];
- const migrations = savedObjectMigrations[type];
- const management = savedObjectsManagement[type];
- const namespaceType = (schema?.isNamespaceAgnostic
- ? 'agnostic'
- : schema?.multiNamespace
- ? 'multiple'
- : 'single') as SavedObjectsNamespaceType;
- return {
- name: type,
- hidden: schema?.hidden ?? false,
- namespaceType,
- mappings,
- indexPattern:
- typeof schema?.indexPattern === 'function'
- ? schema.indexPattern(legacyConfig)
- : schema?.indexPattern,
- convertToAliasScript: schema?.convertToAliasScript,
- migrations: convertLegacyMigrations(migrations ?? {}),
- management: management ? convertLegacyTypeManagement(management) : undefined,
- };
- }),
- ];
- }, [] as SavedObjectsType[]);
-};
-
-/**
- * Convert {@link SavedObjectsType | saved object types} to the legacy {@link SavedObjectsSchemaDefinition | schema} format
- */
-export const convertTypesToLegacySchema = (
- types: SavedObjectsType[]
-): SavedObjectsSchemaDefinition => {
- return types.reduce((schema, type) => {
- return {
- ...schema,
- [type.name]: {
- isNamespaceAgnostic: type.namespaceType === 'agnostic',
- multiNamespace: type.namespaceType === 'multiple',
- hidden: type.hidden,
- indexPattern: type.indexPattern,
- convertToAliasScript: type.convertToAliasScript,
- },
- };
- }, {} as SavedObjectsSchemaDefinition);
-};
-
-const convertLegacyMigrations = (
- legacyMigrations: SavedObjectLegacyMigrationMap
-): SavedObjectMigrationMap => {
- return Object.entries(legacyMigrations).reduce((migrated, [version, migrationFn]) => {
- return {
- ...migrated,
- [version]: (doc, context) => migrationFn(doc, context.log),
- };
- }, {} as SavedObjectMigrationMap);
-};
-
-const convertLegacyTypeManagement = (
- legacyTypeManagement: SavedObjectsLegacyManagementTypeDefinition
-): SavedObjectsTypeManagementDefinition => {
- return {
- importableAndExportable: legacyTypeManagement.isImportableAndExportable,
- defaultSearchField: legacyTypeManagement.defaultSearchField,
- icon: legacyTypeManagement.icon,
- getTitle: legacyTypeManagement.getTitle,
- getEditUrl: legacyTypeManagement.getEditUrl,
- getInAppUrl: legacyTypeManagement.getInAppUrl,
- };
-};
diff --git a/src/core/server/saved_objects/validation/index.ts b/src/core/server/saved_objects/validation/index.ts
deleted file mode 100644
index b1b33f91d3fd4..0000000000000
--- a/src/core/server/saved_objects/validation/index.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/*
- * This is the core logic for validating saved object properties. The saved object client
- * and migrations consume this in order to validate saved object documents prior to
- * persisting them.
- */
-
-interface SavedObjectDoc {
- type: string;
- [prop: string]: any;
-}
-
-/**
- * A dictionary of property name -> validation function. The property name
- * is generally the document's type (e.g. "dashboard"), but will also
- * match other properties.
- *
- * For example, the "acl" and "dashboard" validators both apply to the
- * following saved object: { type: "dashboard", attributes: {}, acl: "sdlaj3w" }
- *
- * @export
- * @interface Validators
- */
-export interface PropertyValidators {
- [prop: string]: ValidateDoc;
-}
-
-export type ValidateDoc = (doc: SavedObjectDoc) => void;
-
-/**
- * Creates a function which uses a dictionary of property validators to validate
- * individual saved object documents.
- *
- * @export
- * @param {Validators} validators
- * @param {SavedObjectDoc} doc
- */
-export function docValidator(validators: PropertyValidators = {}): ValidateDoc {
- return function validateDoc(doc: SavedObjectDoc) {
- Object.keys(doc)
- .concat(doc.type)
- .forEach((prop) => {
- const validator = validators[prop];
- if (validator) {
- validator(doc);
- }
- });
- };
-}
diff --git a/src/core/server/saved_objects/validation/readme.md b/src/core/server/saved_objects/validation/readme.md
deleted file mode 100644
index 3b9f17c37fd0b..0000000000000
--- a/src/core/server/saved_objects/validation/readme.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Saved Object Validations
-
-The saved object client supports validation of documents during create / bulkCreate operations.
-
-This allows us tighter control over what documents get written to the saved object index, and helps us keep the index in a healthy state.
-
-## Creating validations
-
-Plugin authors can write their own validations by adding a `validations` property to their uiExports. A validation is nothing more than a dictionary of `{[prop: string]: validationFunction}` where:
-
-* `prop` - a root-property on a saved object document
-* `validationFunction` - a function that takes a document and throws an error if it does not meet expectations.
-
-## Example
-
-```js
-// In myFanciPlugin...
-uiExports: {
- validations: {
- myProperty(doc) {
- if (doc.attributes.someField === undefined) {
- throw new Error(`Document ${doc.id} did not define "someField"`);
- }
- },
-
- someOtherProp(doc) {
- if (doc.attributes.counter < 0) {
- throw new Error(`Document ${doc.id} cannot have a negative counter.`);
- }
- },
- },
-},
-```
-
-In this example, `myFanciPlugin` defines validations for two properties: `myProperty` and `someOtherProp`.
-
-This means that no other plugin can define validations for myProperty or someOtherProp.
-
-The `myProperty` validation would run for any doc that has a `type="myProperty"` or for any doc that has a root-level property of `myProperty`. e.g. it would apply to all documents in the following array:
-
-```js
-[
- {
- type: 'foo',
- attributes: { stuff: 'here' },
- myProperty: 'shazm!',
- },
- {
- type: 'myProperty',
- attributes: { shazm: true },
- },
-];
-```
-
-Validating properties other than just 'type' allows us to support potential future saved object scenarios in which plugins might want to annotate other plugin documents, such as a security plugin adding an acl to another document:
-
-```js
-{
- type: 'dashboard',
- attributes: { stuff: 'here' },
- acl: '342343',
-}
-```
diff --git a/src/core/server/saved_objects/validation/validation.test.ts b/src/core/server/saved_objects/validation/validation.test.ts
deleted file mode 100644
index 71e220280ba5f..0000000000000
--- a/src/core/server/saved_objects/validation/validation.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { docValidator } from './index';
-
-describe('docValidator', () => {
- test('does not run validators that have no application to the doc', () => {
- const validators = {
- foo: () => {
- throw new Error('Boom!');
- },
- };
- expect(() => docValidator(validators)({ type: 'shoo', bar: 'hi' })).not.toThrow();
- });
-
- test('validates the doc type', () => {
- const validators = {
- foo: () => {
- throw new Error('Boom!');
- },
- };
- expect(() => docValidator(validators)({ type: 'foo' })).toThrow(/Boom!/);
- });
-
- test('validates various props', () => {
- const validators = {
- a: jest.fn(),
- b: jest.fn(),
- c: jest.fn(),
- };
- docValidator(validators)({ type: 'a', b: 'foo' });
-
- expect(validators.c).not.toHaveBeenCalled();
-
- expect(validators.a.mock.calls).toEqual([[{ type: 'a', b: 'foo' }]]);
- expect(validators.b.mock.calls).toEqual([[{ type: 'a', b: 'foo' }]]);
- });
-});
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 081554cd17f25..b86cc14636b8c 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1411,19 +1411,30 @@ export interface LegacyServiceStartDeps {
plugins: Record;
}
-// Warning: (ae-forgotten-export) The symbol "SavedObjectsLegacyUiExports" needs to be exported by the entry point index.d.ts
-//
// @internal @deprecated (undocumented)
-export type LegacyUiExports = SavedObjectsLegacyUiExports & {
+export interface LegacyUiExports {
+ // Warning: (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
defaultInjectedVarProviders?: VarsProvider[];
+ // Warning: (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
injectedVarsReplacers?: VarsReplacer[];
+ // Warning: (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
navLinkSpecs?: LegacyNavLinkSpec[] | null;
+ // Warning: (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
uiAppSpecs?: Array;
+ // (undocumented)
unknown?: [{
pluginSpec: LegacyPluginSpec;
type: unknown;
}];
-};
+}
// Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts
//
@@ -1520,10 +1531,10 @@ export interface LogRecord {
timestamp: Date;
}
-// Warning: (ae-missing-release-tag) "MetricsServiceSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
+// @public
export interface MetricsServiceSetup {
+ readonly collectionInterval: number;
+ getOpsMetrics$: () => Observable;
}
// @public @deprecated (undocumented)
@@ -1610,6 +1621,7 @@ export interface OnPreRoutingToolkit {
// @public
export interface OpsMetrics {
+ collected_at: Date;
concurrent_connections: OpsServerMetrics['concurrent_connections'];
os: OpsOsMetrics;
process: OpsProcessMetrics;
@@ -1619,6 +1631,20 @@ export interface OpsMetrics {
// @public
export interface OpsOsMetrics {
+ cpu?: {
+ control_group: string;
+ cfs_period_micros: number;
+ cfs_quota_micros: number;
+ stat: {
+ number_of_elapsed_periods: number;
+ number_of_times_throttled: number;
+ time_throttled_nanos: number;
+ };
+ };
+ cpuacct?: {
+ control_group: string;
+ usage_nanos: number;
+ };
distro?: string;
distroRelease?: string;
load: {
@@ -2437,33 +2463,6 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt
refresh?: MutatingOperationRefreshSetting;
}
-// @internal @deprecated (undocumented)
-export interface SavedObjectsLegacyService {
- // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientProvider" needs to be exported by the entry point index.d.ts
- //
- // (undocumented)
- addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider['addClientWrapperFactory'];
- // (undocumented)
- getSavedObjectsRepository(...rest: any[]): any;
- // (undocumented)
- getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
- // (undocumented)
- importExport: {
- objectLimit: number;
- importSavedObjects(options: SavedObjectsImportOptions): Promise;
- resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise;
- getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise;
- };
- // (undocumented)
- SavedObjectsClient: typeof SavedObjectsClient;
- // (undocumented)
- schema: SavedObjectsSchema;
- // (undocumented)
- setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory'];
- // (undocumented)
- types: string[];
-}
-
// @public
export interface SavedObjectsMappingProperties {
// (undocumented)
@@ -2517,10 +2516,10 @@ export class SavedObjectsRepository {
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
- // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts
+ // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts
//
// @internal
- static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
+ static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise;
@@ -2548,24 +2547,6 @@ export interface SavedObjectsResolveImportErrorsOptions {
typeRegistry: ISavedObjectTypeRegistry;
}
-// @internal @deprecated (undocumented)
-export class SavedObjectsSchema {
- // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts
- constructor(schemaDefinition?: SavedObjectsSchemaDefinition);
- // (undocumented)
- getConvertToAliasScript(type: string): string | undefined;
- // (undocumented)
- getIndexForType(config: LegacyConfig, type: string): string | undefined;
- // (undocumented)
- isHiddenType(type: string): boolean;
- // (undocumented)
- isMultiNamespace(type: string): boolean;
- // (undocumented)
- isNamespaceAgnostic(type: string): boolean;
- // (undocumented)
- isSingleNamespace(type: string): boolean;
-}
-
// @public
export class SavedObjectsSerializer {
// @internal
@@ -2888,11 +2869,7 @@ export const validBodyOutput: readonly ["data", "stream"];
// Warnings were encountered during analysis:
//
// src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:132:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:133:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:134:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:135:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:136:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:135:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index cc6d8171e7a03..278dd72d72bb1 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -142,7 +142,6 @@ export class Server {
const savedObjectsSetup = await this.savedObjects.setup({
http: httpSetup,
elasticsearch: elasticsearchServiceSetup,
- legacyPlugins,
});
const uiSettingsSetup = await this.uiSettings.setup({
diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
index 61b71f8c5de07..c7d5413ecca56 100644
--- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
+++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
@@ -36,8 +36,6 @@ describe('createOrUpgradeSavedConfig()', () => {
let esServer: TestElasticsearchUtils;
let kbn: TestKibanaUtils;
- let kbnServer: TestKibanaUtils['kbnServer'];
-
beforeAll(async function () {
servers = createTestServers({
adjustTimeout: (t) => {
@@ -46,10 +44,8 @@ describe('createOrUpgradeSavedConfig()', () => {
});
esServer = await servers.startES();
kbn = await servers.startKibana();
- kbnServer = kbn.kbnServer;
- const savedObjects = kbnServer.server.savedObjects;
- savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
+ savedObjectsClient = kbn.coreStart.savedObjects.getScopedClient(
httpServerMock.createKibanaRequest()
);
diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts
index 297deb0233c57..0bdc821f42581 100644
--- a/src/core/server/ui_settings/integration_tests/lib/servers.ts
+++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts
@@ -68,8 +68,7 @@ export function getServices() {
const callCluster = esServer.es.getCallCluster();
- const savedObjects = kbnServer.server.savedObjects;
- const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
+ const savedObjectsClient = kbn.coreStart.savedObjects.getScopedClient(
httpServerMock.createKibanaRequest()
);
diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts
index a494c6aa31d6f..488c4b919d3e4 100644
--- a/src/core/test_helpers/kbn_server.ts
+++ b/src/core/test_helpers/kbn_server.ts
@@ -32,6 +32,7 @@ import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import supertest from 'supertest';
+import { CoreStart } from 'src/core/server';
import { LegacyAPICaller } from '../server/elasticsearch';
import { CliArgs, Env } from '../server/config';
import { Root } from '../server/root';
@@ -170,6 +171,7 @@ export interface TestElasticsearchUtils {
export interface TestKibanaUtils {
root: Root;
+ coreStart: CoreStart;
kbnServer: KbnServer;
stop: () => Promise;
}
@@ -289,13 +291,14 @@ export function createTestServers({
const root = createRootWithCorePlugins(kbnSettings);
await root.setup();
- await root.start();
+ const coreStart = await root.start();
const kbnServer = getKbnServer(root);
return {
root,
kbnServer,
+ coreStart,
stop: async () => await root.shutdown(),
};
},
diff --git a/src/fixtures/stubbed_saved_object_index_pattern.ts b/src/fixtures/stubbed_saved_object_index_pattern.ts
index 02e6cb85e341f..44b391f14cf9c 100644
--- a/src/fixtures/stubbed_saved_object_index_pattern.ts
+++ b/src/fixtures/stubbed_saved_object_index_pattern.ts
@@ -30,6 +30,7 @@ export function stubbedSavedObjectIndexPattern(id: string | null = null) {
timeFieldName: 'timestamp',
customFormats: '{}',
fields: mockLogstashFields,
+ title: 'title',
},
version: 2,
};
diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js
index 599886788604b..f90f490d68035 100644
--- a/src/legacy/core_plugins/elasticsearch/index.js
+++ b/src/legacy/core_plugins/elasticsearch/index.js
@@ -16,18 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { first } from 'rxjs/operators';
import { Cluster } from './server/lib/cluster';
import { createProxy } from './server/lib/create_proxy';
export default function (kibana) {
- let defaultVars;
-
return new kibana.Plugin({
require: [],
- uiExports: { injectDefaultVars: () => defaultVars },
-
async init(server) {
// All methods that ES plugin exposes are synchronous so we should get the first
// value from all observables here to be able to synchronously return and create
@@ -36,16 +31,6 @@ export default function (kibana) {
const adminCluster = new Cluster(client);
const dataCluster = new Cluster(client);
- const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
- .pipe(first())
- .toPromise();
-
- defaultVars = {
- esRequestTimeout: esConfig.requestTimeout.asMilliseconds(),
- esShardTimeout: esConfig.shardTimeout.asMilliseconds(),
- esApiVersion: esConfig.apiVersion,
- };
-
const clusters = new Map();
server.expose('getCluster', (name) => {
if (name === 'admin') {
diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts
index e51a355cbc8d2..e1ed2f57375a4 100644
--- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts
+++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts
@@ -18,14 +18,10 @@
*/
import { Server } from '../../server/kbn_server';
import { Capabilities } from '../../../core/server';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { SavedObjectsLegacyManagementDefinition } from '../../../core/server/saved_objects/types';
export type InitPluginFunction = (server: Server) => void;
export interface UiExports {
injectDefaultVars?: (server: Server) => { [key: string]: any };
- savedObjectsManagement?: SavedObjectsLegacyManagementDefinition;
- mappings?: unknown;
}
export interface PluginSpecOptions {
diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts
index 283806f69599a..700ca6fa68c95 100644
--- a/src/legacy/plugin_discovery/types.ts
+++ b/src/legacy/plugin_discovery/types.ts
@@ -19,11 +19,6 @@
import { Server } from '../server/kbn_server';
import { Capabilities } from '../../core/server';
-// Disable lint errors for imports from src/core/* until SavedObjects migration is complete
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { SavedObjectsLegacyManagementDefinition } from '../../core/server/saved_objects/types';
import { AppCategory } from '../../core/types';
/**
@@ -70,8 +65,6 @@ export interface LegacyPluginOptions {
home: string[];
mappings: any;
migrations: any;
- savedObjectSchemas: SavedObjectsSchemaDefinition;
- savedObjectsManagement: SavedObjectsLegacyManagementDefinition;
visTypes: string[];
embeddableActions?: string[];
embeddableFactories?: string[];
diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts
index 69fb63fbbd87f..663542618375a 100644
--- a/src/legacy/server/kbn_server.d.ts
+++ b/src/legacy/server/kbn_server.d.ts
@@ -17,33 +17,24 @@
* under the License.
*/
-import { ResponseObject, Server } from 'hapi';
-import { UnwrapPromise } from '@kbn/utility-types';
+import { Server } from 'hapi';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import {
- ConfigService,
CoreSetup,
CoreStart,
- ElasticsearchServiceSetup,
EnvironmentMode,
LoggerFactory,
- SavedObjectsClientContract,
- SavedObjectsLegacyService,
- SavedObjectsClientProviderOptions,
- IUiSettingsClient,
PackageInfo,
- LegacyRequest,
LegacyServiceSetupDeps,
- LegacyServiceStartDeps,
LegacyServiceDiscoverPlugins,
} from '../../core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy';
+import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { UiPlugins } from '../../core/server/plugins';
-import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
+import { ElasticsearchPlugin } from '../core_plugins/elasticsearch';
import { UsageCollectionSetup } from '../../plugins/usage_collection/server';
import { HomeServerPluginSetup } from '../../plugins/home/server';
@@ -61,16 +52,9 @@ declare module 'hapi' {
interface Server {
config: () => KibanaConfig;
- savedObjects: SavedObjectsLegacyService;
logWithMetadata: (tags: string[], message: string, meta: Record) => void;
newPlatform: KbnServer['newPlatform'];
}
-
- interface Request {
- getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract;
- getBasePath(): string;
- getUiSettingsService(): IUiSettingsClient;
- }
}
type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void;
@@ -86,11 +70,9 @@ export interface KibanaCore {
__internals: {
elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch'];
hapiServer: LegacyServiceSetupDeps['core']['http']['server'];
- kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator'];
legacy: ILegacyInternals;
rendering: LegacyServiceSetupDeps['core']['rendering'];
uiPlugins: UiPlugins;
- savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider'];
};
env: {
mode: Readonly;
@@ -149,6 +131,3 @@ export default class KbnServer {
// Re-export commonly used hapi types.
export { Server, Request, ResponseToolkit } from 'hapi';
-
-// Re-export commonly accessed api types.
-export { SavedObjectsLegacyService, SavedObjectsClient } from 'src/core/server';
diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js
index 4692262d99bb5..a5eefd140c8fa 100644
--- a/src/legacy/server/kbn_server.js
+++ b/src/legacy/server/kbn_server.js
@@ -33,7 +33,6 @@ import pidMixin from './pid';
import configCompleteMixin from './config/complete';
import { optimizeMixin } from '../../optimize';
import * as Plugins from './plugins';
-import { savedObjectsMixin } from './saved_objects/saved_objects_mixin';
import { uiMixin } from '../ui';
import { i18nMixin } from './i18n';
@@ -108,9 +107,6 @@ export default class KbnServer {
uiMixin,
- // setup saved object routes
- savedObjectsMixin,
-
// setup routes that serve the @kbn/optimizer output
optimizeMixin,
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js
deleted file mode 100644
index 96cf2058839cf..0000000000000
--- a/src/legacy/server/saved_objects/saved_objects_mixin.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete
-/* eslint-disable @kbn/eslint/no-restricted-paths */
-import { SavedObjectsSchema } from '../../../core/server/saved_objects/schema';
-import {
- SavedObjectsClient,
- SavedObjectsRepository,
- exportSavedObjectsToStream,
- importSavedObjectsFromStream,
- resolveSavedObjectsImportErrors,
-} from '../../../core/server/saved_objects';
-import { convertTypesToLegacySchema } from '../../../core/server/saved_objects/utils';
-
-export function savedObjectsMixin(kbnServer, server) {
- const migrator = kbnServer.newPlatform.__internals.kibanaMigrator;
- const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry();
- const mappings = migrator.getActiveMappings();
- const allTypes = typeRegistry.getAllTypes().map((t) => t.name);
- const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name);
- const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes()));
-
- server.decorate('server', 'kibanaMigrator', migrator);
-
- const serializer = kbnServer.newPlatform.start.core.savedObjects.createSerializer();
-
- const createRepository = (callCluster, includedHiddenTypes = []) => {
- if (typeof callCluster !== 'function') {
- throw new TypeError('Repository requires a "callCluster" function to be provided.');
- }
- // throw an exception if an extraType is not defined.
- includedHiddenTypes.forEach((type) => {
- if (!allTypes.includes(type)) {
- throw new Error(`Missing mappings for saved objects type '${type}'`);
- }
- });
- const combinedTypes = visibleTypes.concat(includedHiddenTypes);
- const allowedTypes = [...new Set(combinedTypes)];
-
- const config = server.config();
-
- return new SavedObjectsRepository({
- index: config.get('kibana.index'),
- migrator,
- mappings,
- typeRegistry,
- serializer,
- allowedTypes,
- callCluster,
- });
- };
-
- const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider;
-
- const service = {
- types: visibleTypes,
- SavedObjectsClient,
- SavedObjectsRepository,
- getSavedObjectsRepository: createRepository,
- getScopedSavedObjectsClient: (...args) => provider.getClient(...args),
- setScopedSavedObjectsClientFactory: (...args) => provider.setClientFactory(...args),
- addScopedSavedObjectsClientWrapperFactory: (...args) =>
- provider.addClientWrapperFactory(...args),
- importExport: {
- objectLimit: server.config().get('savedObjects.maxImportExportSize'),
- importSavedObjects: importSavedObjectsFromStream,
- resolveImportErrors: resolveSavedObjectsImportErrors,
- getSortedObjectsForExport: exportSavedObjectsToStream,
- },
- schema,
- };
- server.decorate('server', 'savedObjects', service);
-
- const savedObjectsClientCache = new WeakMap();
- server.decorate('request', 'getSavedObjectsClient', function (options) {
- const request = this;
-
- if (savedObjectsClientCache.has(request)) {
- return savedObjectsClientCache.get(request);
- }
-
- const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request, options);
-
- savedObjectsClientCache.set(request, savedObjectsClient);
- return savedObjectsClient;
- });
-}
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
deleted file mode 100644
index d1d6c052ad589..0000000000000
--- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { savedObjectsMixin } from './saved_objects_mixin';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { mockKibanaMigrator } from '../../../core/server/saved_objects/migrations/kibana/kibana_migrator.mock';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { savedObjectsClientProviderMock } from '../../../core/server/saved_objects/service/lib/scoped_client_provider.mock';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { convertLegacyTypes } from '../../../core/server/saved_objects/utils';
-import { SavedObjectTypeRegistry } from '../../../core/server';
-import { coreMock } from '../../../core/server/mocks';
-
-const mockConfig = {
- get: jest.fn().mockReturnValue('anything'),
-};
-
-const savedObjectMappings = [
- {
- pluginId: 'testtype',
- properties: {
- testtype: {
- properties: {
- name: { type: 'keyword' },
- },
- },
- },
- },
- {
- pluginId: 'testtype2',
- properties: {
- doc1: {
- properties: {
- name: { type: 'keyword' },
- },
- },
- doc2: {
- properties: {
- name: { type: 'keyword' },
- },
- },
- },
- },
- {
- pluginId: 'secretPlugin',
- properties: {
- hiddentype: {
- properties: {
- secret: { type: 'keyword' },
- },
- },
- },
- },
-];
-
-const savedObjectSchemas = {
- hiddentype: {
- hidden: true,
- },
- doc1: {
- indexPattern: 'other-index',
- },
-};
-
-const savedObjectTypes = convertLegacyTypes(
- {
- savedObjectMappings,
- savedObjectSchemas,
- savedObjectMigrations: {},
- },
- mockConfig
-);
-
-const typeRegistry = new SavedObjectTypeRegistry();
-savedObjectTypes.forEach((type) => typeRegistry.registerType(type));
-
-const migrator = mockKibanaMigrator.create({
- types: savedObjectTypes,
-});
-
-describe('Saved Objects Mixin', () => {
- let mockKbnServer;
- let mockServer;
- const mockCallCluster = jest.fn();
- const stubCallCluster = jest.fn();
- const config = {
- 'kibana.index': 'kibana.index',
- 'savedObjects.maxImportExportSize': 10000,
- };
- const stubConfig = jest.fn((key) => {
- return config[key];
- });
-
- beforeEach(() => {
- const clientProvider = savedObjectsClientProviderMock.create();
- mockServer = {
- log: jest.fn(),
- route: jest.fn(),
- decorate: jest.fn(),
- config: () => {
- return {
- get: stubConfig,
- };
- },
- plugins: {
- elasticsearch: {
- getCluster: () => {
- return {
- callWithRequest: mockCallCluster,
- callWithInternalUser: stubCallCluster,
- };
- },
- waitUntilReady: jest.fn(),
- },
- },
- };
-
- const coreStart = coreMock.createStart();
- coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
-
- mockKbnServer = {
- newPlatform: {
- __internals: {
- kibanaMigrator: migrator,
- savedObjectsClientProvider: clientProvider,
- },
- setup: {
- core: coreMock.createSetup(),
- },
- start: {
- core: coreStart,
- },
- },
- server: mockServer,
- ready: () => {},
- pluginSpecs: {
- some: () => {
- return true;
- },
- },
- uiExports: {
- savedObjectMappings,
- savedObjectSchemas,
- },
- };
- });
-
- describe('Saved object service', () => {
- let service;
-
- beforeEach(async () => {
- await savedObjectsMixin(mockKbnServer, mockServer);
- const call = mockServer.decorate.mock.calls.filter(
- ([objName, methodName]) => objName === 'server' && methodName === 'savedObjects'
- );
- service = call[0][2];
- });
-
- it('should return all but hidden types', async () => {
- expect(service).toBeDefined();
- expect(service.types).toEqual(['testtype', 'doc1', 'doc2']);
- });
-
- const mockCallEs = jest.fn();
- describe('repository creation', () => {
- it('should not allow a repository with an undefined type', () => {
- expect(() => {
- service.getSavedObjectsRepository(mockCallEs, ['extraType']);
- }).toThrow(new Error("Missing mappings for saved objects type 'extraType'"));
- });
-
- it('should create a repository without hidden types', () => {
- const repository = service.getSavedObjectsRepository(mockCallEs);
- expect(repository).toBeDefined();
- expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']);
- });
-
- it('should create a repository with a unique list of allowed types', () => {
- const repository = service.getSavedObjectsRepository(mockCallEs, ['doc1', 'doc1', 'doc1']);
- expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']);
- });
-
- it('should create a repository with extraTypes minus duplicate', () => {
- const repository = service.getSavedObjectsRepository(mockCallEs, [
- 'hiddentype',
- 'hiddentype',
- ]);
- expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2', 'hiddentype']);
- });
-
- it('should not allow a repository without a callCluster function', () => {
- expect(() => {
- service.getSavedObjectsRepository({});
- }).toThrow(new Error('Repository requires a "callCluster" function to be provided.'));
- });
- });
-
- describe('get client', () => {
- it('should have a method to get the client', () => {
- expect(service).toHaveProperty('getScopedSavedObjectsClient');
- });
-
- it('should have a method to set the client factory', () => {
- expect(service).toHaveProperty('setScopedSavedObjectsClientFactory');
- });
-
- it('should have a method to add a client wrapper factory', () => {
- expect(service).toHaveProperty('addScopedSavedObjectsClientWrapperFactory');
- });
-
- it('should allow you to set a scoped saved objects client factory', () => {
- expect(() => {
- service.setScopedSavedObjectsClientFactory({});
- }).not.toThrowError();
- });
-
- it('should allow you to add a scoped saved objects client wrapper factory', () => {
- expect(() => {
- service.addScopedSavedObjectsClientWrapperFactory({});
- }).not.toThrowError();
- });
- });
-
- describe('#getSavedObjectsClient', () => {
- let getSavedObjectsClient;
-
- beforeEach(() => {
- savedObjectsMixin(mockKbnServer, mockServer);
- const call = mockServer.decorate.mock.calls.filter(
- ([objName, methodName]) => objName === 'request' && methodName === 'getSavedObjectsClient'
- );
- getSavedObjectsClient = call[0][2];
- });
-
- it('should be callable', () => {
- mockServer.savedObjects = service;
- getSavedObjectsClient = getSavedObjectsClient.bind({});
- expect(() => {
- getSavedObjectsClient();
- }).not.toThrowError();
- });
-
- it('should use cached request object', () => {
- mockServer.savedObjects = service;
- getSavedObjectsClient = getSavedObjectsClient.bind({ _test: 'me' });
- const savedObjectsClient = getSavedObjectsClient();
- expect(getSavedObjectsClient()).toEqual(savedObjectsClient);
- });
- });
- });
-});
diff --git a/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js b/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js
deleted file mode 100644
index 5b2af9f82333c..0000000000000
--- a/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import expect from '@kbn/expect';
-
-import { PluginPack } from '../../../plugin_discovery';
-
-import { collectUiExports } from '../collect_ui_exports';
-
-const specs = new PluginPack({
- path: '/dev/null',
- pkg: {
- name: 'test',
- version: 'kibana',
- },
- provider({ Plugin }) {
- return [
- new Plugin({
- id: 'test',
- uiExports: {
- savedObjectSchemas: {
- foo: {
- isNamespaceAgnostic: true,
- },
- },
- },
- }),
- new Plugin({
- id: 'test2',
- uiExports: {
- savedObjectSchemas: {
- bar: {
- isNamespaceAgnostic: true,
- },
- },
- },
- }),
- ];
- },
-}).getPluginSpecs();
-
-describe('plugin discovery', () => {
- describe('collectUiExports()', () => {
- it('merges uiExports from all provided plugin specs', () => {
- const uiExports = collectUiExports(specs);
-
- expect(uiExports.savedObjectSchemas).to.eql({
- foo: {
- isNamespaceAgnostic: true,
- },
- bar: {
- isNamespaceAgnostic: true,
- },
- });
- });
-
- it(`throws an error when migrations and mappings aren't defined in the same plugin`, () => {
- const invalidSpecs = new PluginPack({
- path: '/dev/null',
- pkg: {
- name: 'test',
- version: 'kibana',
- },
- provider({ Plugin }) {
- return [
- new Plugin({
- id: 'test',
- uiExports: {
- mappings: {
- 'test-type': {
- properties: {},
- },
- },
- },
- }),
- new Plugin({
- id: 'test2',
- uiExports: {
- migrations: {
- 'test-type': {
- '1.2.3': (doc) => {
- return doc;
- },
- },
- },
- },
- }),
- ];
- },
- }).getPluginSpecs();
- expect(() => collectUiExports(invalidSpecs)).to.throwError((err) => {
- expect(err).to.be.a(Error);
- expect(err).to.have.property(
- 'message',
- 'Migrations and mappings must be defined together in the uiExports of a single plugin. ' +
- 'test2 defines migrations for types test-type but does not define their mappings.'
- );
- });
- });
- });
-});
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index cd8dcf5aff71d..e3b7c1e0c3ff9 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -193,13 +193,11 @@ export function uiRenderMixin(kbnServer, server, config) {
async function renderApp(h) {
const app = { getId: () => 'core' };
const { http } = kbnServer.newPlatform.setup.core;
- const {
- rendering,
- legacy,
- savedObjectsClientProvider: savedObjects,
- } = kbnServer.newPlatform.__internals;
+ const { savedObjects } = kbnServer.newPlatform.start.core;
+ const { rendering, legacy } = kbnServer.newPlatform.__internals;
+ const req = KibanaRequest.from(h.request);
const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(
- savedObjects.getClient(h.request)
+ savedObjects.getScopedClient(req)
);
const vars = await legacy.getVars(app.getId(), h.request, {
apmConfig: getApmConfig(h.request.path),
diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js
index 529b1ddfd8a4d..e2e2331b3aea6 100644
--- a/src/legacy/utils/index.js
+++ b/src/legacy/utils/index.js
@@ -17,8 +17,6 @@
* under the License.
*/
-export { BinderBase } from './binder';
-export { BinderFor } from './binder_for';
export { deepCloneWithBuffers } from './deep_clone_with_buffers';
export { unset } from './unset';
export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type';
diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts
index 22db1552e4303..43120583bd3a4 100644
--- a/src/plugins/data/common/constants.ts
+++ b/src/plugins/data/common/constants.ts
@@ -32,6 +32,7 @@ export const UI_SETTINGS = {
COURIER_MAX_CONCURRENT_SHARD_REQUESTS: 'courier:maxConcurrentShardRequests',
COURIER_BATCH_SEARCHES: 'courier:batchSearches',
SEARCH_INCLUDE_FROZEN: 'search:includeFrozen',
+ SEARCH_TIMEOUT: 'search:timeout',
HISTOGRAM_BAR_TARGET: 'histogram:barTarget',
HISTOGRAM_MAX_BARS: 'histogram:maxBars',
HISTORY_LIMIT: 'history:limit',
diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts
index d6205d54bd702..69163842f3498 100644
--- a/src/plugins/data/common/field_formats/converters/duration.test.ts
+++ b/src/plugins/data/common/field_formats/converters/duration.test.ts
@@ -24,11 +24,16 @@ describe('Duration Format', () => {
inputFormat: 'seconds',
outputFormat: 'humanize',
outputPrecision: undefined,
+ showSuffix: undefined,
fixtures: [
{
input: -60,
output: 'minus a minute',
},
+ {
+ input: 1,
+ output: 'a few seconds',
+ },
{
input: 60,
output: 'a minute',
@@ -44,6 +49,7 @@ describe('Duration Format', () => {
inputFormat: 'minutes',
outputFormat: 'humanize',
outputPrecision: undefined,
+ showSuffix: undefined,
fixtures: [
{
input: -60,
@@ -64,6 +70,7 @@ describe('Duration Format', () => {
inputFormat: 'minutes',
outputFormat: 'asHours',
outputPrecision: undefined,
+ showSuffix: undefined,
fixtures: [
{
input: -60,
@@ -84,6 +91,7 @@ describe('Duration Format', () => {
inputFormat: 'seconds',
outputFormat: 'asSeconds',
outputPrecision: 0,
+ showSuffix: undefined,
fixtures: [
{
input: -60,
@@ -104,6 +112,7 @@ describe('Duration Format', () => {
inputFormat: 'seconds',
outputFormat: 'asSeconds',
outputPrecision: 2,
+ showSuffix: undefined,
fixtures: [
{
input: -60,
@@ -124,15 +133,34 @@ describe('Duration Format', () => {
],
});
+ testCase({
+ inputFormat: 'seconds',
+ outputFormat: 'asSeconds',
+ outputPrecision: 0,
+ showSuffix: true,
+ fixtures: [
+ {
+ input: -60,
+ output: '-60 Seconds',
+ },
+ {
+ input: -32.333,
+ output: '-32 Seconds',
+ },
+ ],
+ });
+
function testCase({
inputFormat,
outputFormat,
outputPrecision,
+ showSuffix,
fixtures,
}: {
inputFormat: string;
outputFormat: string;
outputPrecision: number | undefined;
+ showSuffix: boolean | undefined;
fixtures: any[];
}) {
fixtures.forEach((fixture: Record) => {
@@ -143,7 +171,7 @@ describe('Duration Format', () => {
outputPrecision ? `, ${outputPrecision} decimals` : ''
}`, () => {
const duration = new DurationFormat(
- { inputFormat, outputFormat, outputPrecision },
+ { inputFormat, outputFormat, outputPrecision, showSuffix },
jest.fn()
);
expect(duration.convert(input)).toBe(output);
diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts
index 53c2aba98120e..a3ce3d4dfd795 100644
--- a/src/plugins/data/common/field_formats/converters/duration.ts
+++ b/src/plugins/data/common/field_formats/converters/duration.ts
@@ -190,6 +190,7 @@ export class DurationFormat extends FieldFormat {
const inputFormat = this.param('inputFormat');
const outputFormat = this.param('outputFormat') as keyof Duration;
const outputPrecision = this.param('outputPrecision');
+ const showSuffix = Boolean(this.param('showSuffix'));
const human = this.isHuman();
const prefix =
val < 0 && human
@@ -200,6 +201,9 @@ export class DurationFormat extends FieldFormat {
const duration = parseInputAsDuration(val, inputFormat) as Record;
const formatted = duration[outputFormat]();
const precise = human ? formatted : formatted.toFixed(outputPrecision);
- return prefix + precise;
+ const type = outputFormats.find(({ method }) => method === outputFormat);
+ const suffix = showSuffix && type ? ` ${type.text}` : '';
+
+ return prefix + precise + suffix;
};
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
index a0c380ec55bf6..1871627da76de 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
@@ -631,7 +631,7 @@ Object {
"id": "test-pattern",
"sourceFilters": undefined,
"timeFieldName": "timestamp",
- "title": "test-pattern",
+ "title": "title",
"typeMeta": undefined,
"version": 2,
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
index f037a71b508a2..f49897c47d562 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { defaults, map, last, get } from 'lodash';
+import { defaults, map, last } from 'lodash';
import { IndexPattern } from './index_pattern';
@@ -29,7 +29,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_
import { IndexPatternField } from '../fields';
import { fieldFormatsMock } from '../../field_formats/mocks';
-import { FieldFormat } from '../..';
+import { FieldFormat, IndexPatternsService } from '../..';
class MockFieldFormatter {}
@@ -116,6 +116,7 @@ function create(id: string, payload?: any): Promise {
apiClient,
patternCache,
fieldFormats: fieldFormatsMock,
+ indexPatternsService: {} as IndexPatternsService,
onNotification: () => {},
onError: () => {},
shortDotsEnable: false,
@@ -151,7 +152,6 @@ describe('IndexPattern', () => {
expect(indexPattern).toHaveProperty('getNonScriptedFields');
expect(indexPattern).toHaveProperty('addScriptedField');
expect(indexPattern).toHaveProperty('removeScriptedField');
- expect(indexPattern).toHaveProperty('save');
// properties
expect(indexPattern).toHaveProperty('fields');
@@ -389,60 +389,4 @@ describe('IndexPattern', () => {
});
});
});
-
- test('should handle version conflicts', async () => {
- setDocsourcePayload(null, {
- id: 'foo',
- version: 'foo',
- attributes: {
- title: 'something',
- },
- });
- // Create a normal index pattern
- const pattern = new IndexPattern('foo', {
- savedObjectsClient: savedObjectsClient as any,
- apiClient,
- patternCache,
- fieldFormats: fieldFormatsMock,
- onNotification: () => {},
- onError: () => {},
- shortDotsEnable: false,
- metaFields: [],
- });
- await pattern.init();
-
- expect(get(pattern, 'version')).toBe('fooa');
-
- // Create the same one - we're going to handle concurrency
- const samePattern = new IndexPattern('foo', {
- savedObjectsClient: savedObjectsClient as any,
- apiClient,
- patternCache,
- fieldFormats: fieldFormatsMock,
- onNotification: () => {},
- onError: () => {},
- shortDotsEnable: false,
- metaFields: [],
- });
- await samePattern.init();
-
- expect(get(samePattern, 'version')).toBe('fooaa');
-
- // This will conflict because samePattern did a save (from refreshFields)
- // but the resave should work fine
- pattern.title = 'foo2';
- await pattern.save();
-
- // This should not be able to recover
- samePattern.title = 'foo3';
-
- let result;
- try {
- await samePattern.save();
- } catch (err) {
- result = err;
- }
-
- expect(result.res.status).toBe(409);
- });
});
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index 0558808573580..76f1a5e59d0ee 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -41,8 +41,8 @@ import { PatternCache } from './_pattern_cache';
import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping';
import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types';
import { SerializedFieldFormat } from '../../../../expressions/common';
+import { IndexPatternsService } from '..';
-const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
const savedObjectType = 'index-pattern';
interface IndexPatternDeps {
@@ -50,6 +50,7 @@ interface IndexPatternDeps {
apiClient: IIndexPatternsApiClient;
patternCache: PatternCache;
fieldFormats: FieldFormatsStartCommon;
+ indexPatternsService: IndexPatternsService;
onNotification: OnNotification;
onError: OnError;
shortDotsEnable: boolean;
@@ -70,17 +71,18 @@ export class IndexPattern implements IIndexPattern {
public flattenHit: any;
public metaFields: string[];
- private version: string | undefined;
+ public version: string | undefined;
private savedObjectsClient: SavedObjectsClientCommon;
private patternCache: PatternCache;
public sourceFilters?: SourceFilter[];
- private originalBody: { [key: string]: any } = {};
+ // todo make read only, update via method or factor out
+ public originalBody: { [key: string]: any } = {};
public fieldsFetcher: any; // probably want to factor out any direct usage and change to private
+ private indexPatternsService: IndexPatternsService;
private shortDotsEnable: boolean = false;
private fieldFormats: FieldFormatsStartCommon;
private onNotification: OnNotification;
private onError: OnError;
- private apiClient: IIndexPatternsApiClient;
private mapping: MappingObject = expandShorthand({
title: ES_FIELD_TYPES.TEXT,
@@ -111,6 +113,7 @@ export class IndexPattern implements IIndexPattern {
apiClient,
patternCache,
fieldFormats,
+ indexPatternsService,
onNotification,
onError,
shortDotsEnable = false,
@@ -121,6 +124,7 @@ export class IndexPattern implements IIndexPattern {
this.savedObjectsClient = savedObjectsClient;
this.patternCache = patternCache;
this.fieldFormats = fieldFormats;
+ this.indexPatternsService = indexPatternsService;
this.onNotification = onNotification;
this.onError = onError;
@@ -128,7 +132,6 @@ export class IndexPattern implements IIndexPattern {
this.metaFields = metaFields;
this.fields = fieldList([], this.shortDotsEnable);
- this.apiClient = apiClient;
this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields);
this.flattenHit = flattenHitWrapper(this, metaFields);
this.formatHit = formatHitProvider(
@@ -392,8 +395,6 @@ export class IndexPattern implements IIndexPattern {
} else {
throw err;
}
-
- await this.save();
}
}
@@ -402,7 +403,6 @@ export class IndexPattern implements IIndexPattern {
if (field) {
this.fields.remove(field);
}
- return this.save();
}
async popularizeField(fieldName: string, unit = 1) {
@@ -523,92 +523,6 @@ export class IndexPattern implements IIndexPattern {
return await _create(potentialDuplicateByTitle.id);
}
- async save(saveAttempts: number = 0): Promise {
- if (!this.id) return;
- const body = this.prepBody();
-
- const originalChangedKeys: string[] = [];
- Object.entries(body).forEach(([key, value]) => {
- if (value !== this.originalBody[key]) {
- originalChangedKeys.push(key);
- }
- });
-
- return this.savedObjectsClient
- .update(savedObjectType, this.id, body, { version: this.version })
- .then((resp) => {
- this.id = resp.id;
- this.version = resp.version;
- })
- .catch((err) => {
- if (
- _.get(err, 'res.status') === 409 &&
- saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS
- ) {
- const samePattern = new IndexPattern(this.id, {
- savedObjectsClient: this.savedObjectsClient,
- apiClient: this.apiClient,
- patternCache: this.patternCache,
- fieldFormats: this.fieldFormats,
- onNotification: this.onNotification,
- onError: this.onError,
- shortDotsEnable: this.shortDotsEnable,
- metaFields: this.metaFields,
- });
-
- return samePattern.init().then(() => {
- // What keys changed from now and what the server returned
- const updatedBody = samePattern.prepBody();
-
- // Build a list of changed keys from the server response
- // and ensure we ignore the key if the server response
- // is the same as the original response (since that is expected
- // if we made a change in that key)
-
- const serverChangedKeys: string[] = [];
- Object.entries(updatedBody).forEach(([key, value]) => {
- if (value !== (body as any)[key] && value !== this.originalBody[key]) {
- serverChangedKeys.push(key);
- }
- });
-
- let unresolvedCollision = false;
- for (const originalKey of originalChangedKeys) {
- for (const serverKey of serverChangedKeys) {
- if (originalKey === serverKey) {
- unresolvedCollision = true;
- break;
- }
- }
- }
-
- if (unresolvedCollision) {
- const title = i18n.translate('data.indexPatterns.unableWriteLabel', {
- defaultMessage:
- 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.',
- });
-
- this.onNotification({ title, color: 'danger' });
- throw err;
- }
-
- // Set the updated response on this object
- serverChangedKeys.forEach((key) => {
- (this as any)[key] = (samePattern as any)[key];
- });
- this.version = samePattern.version;
-
- // Clear cache
- this.patternCache.clear(this.id!);
-
- // Try the save again
- return this.save(saveAttempts);
- });
- }
- throw err;
- });
- }
-
async _fetchFields() {
const fields = await this.fieldsFetcher.fetch(this);
const scripted = this.getScriptedFields().map((field) => field.spec);
@@ -624,30 +538,37 @@ export class IndexPattern implements IIndexPattern {
}
refreshFields() {
- return this._fetchFields()
- .then(() => this.save())
- .catch((err) => {
- // https://github.com/elastic/kibana/issues/9224
- // This call will attempt to remap fields from the matching
- // ES index which may not actually exist. In that scenario,
- // we still want to notify the user that there is a problem
- // but we do not want to potentially make any pages unusable
- // so do not rethrow the error here
-
- if (err instanceof IndexPatternMissingIndices) {
- this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' });
- return [];
- }
+ return (
+ this._fetchFields()
+ // todo
+ .then(() => this.indexPatternsService.save(this))
+ .catch((err) => {
+ // https://github.com/elastic/kibana/issues/9224
+ // This call will attempt to remap fields from the matching
+ // ES index which may not actually exist. In that scenario,
+ // we still want to notify the user that there is a problem
+ // but we do not want to potentially make any pages unusable
+ // so do not rethrow the error here
+
+ if (err instanceof IndexPatternMissingIndices) {
+ this.onNotification({
+ title: (err as any).message,
+ color: 'danger',
+ iconType: 'alert',
+ });
+ return [];
+ }
- this.onError(err, {
- title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', {
- defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})',
- values: {
- id: this.id,
- title: this.title,
- },
- }),
- });
- });
+ this.onError(err, {
+ title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', {
+ defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})',
+ values: {
+ id: this.id,
+ title: this.title,
+ },
+ }),
+ });
+ })
+ );
}
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
index 8223b31042124..c79c7900148ea 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
@@ -17,28 +17,26 @@
* under the License.
*/
-import { IndexPatternsService } from './index_patterns';
+import { defaults } from 'lodash';
+import { IndexPatternsService } from '.';
import { fieldFormatsMock } from '../../field_formats/mocks';
-import {
- UiSettingsCommon,
- IIndexPatternsApiClient,
- SavedObjectsClientCommon,
- SavedObject,
-} from '../types';
+import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern';
+import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types';
+
+const createFieldsFetcher = jest.fn().mockImplementation(() => ({
+ getFieldsForWildcard: jest.fn().mockImplementation(() => {
+ return new Promise((resolve) => resolve([]));
+ }),
+ every: jest.fn(),
+}));
const fieldFormats = fieldFormatsMock;
-jest.mock('./index_pattern', () => {
- class IndexPattern {
- init = async () => {
- return this;
- };
- }
+let object: any = {};
- return {
- IndexPattern,
- };
-});
+function setDocsourcePayload(id: string | null, providedPayload: any) {
+ object = defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id));
+}
describe('IndexPatterns', () => {
let indexPatterns: IndexPatternsService;
@@ -53,6 +51,25 @@ describe('IndexPatterns', () => {
>
);
savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise);
+ savedObjectsClient.get = jest.fn().mockImplementation(() => object);
+ savedObjectsClient.create = jest.fn();
+ savedObjectsClient.update = jest
+ .fn()
+ .mockImplementation(async (type, id, body, { version }) => {
+ if (object.version !== version) {
+ throw new Object({
+ res: {
+ status: 409,
+ },
+ });
+ }
+ object.attributes.title = body.title;
+ object.version += 'a';
+ return {
+ id: object.id,
+ version: object.version,
+ };
+ });
indexPatterns = new IndexPatternsService({
uiSettings: ({
@@ -60,7 +77,7 @@ describe('IndexPatterns', () => {
getAll: () => {},
} as any) as UiSettingsCommon,
savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientCommon,
- apiClient: {} as IIndexPatternsApiClient,
+ apiClient: createFieldsFetcher(),
fieldFormats,
onNotification: () => {},
onError: () => {},
@@ -70,6 +87,14 @@ describe('IndexPatterns', () => {
test('does cache gets for the same id', async () => {
const id = '1';
+ setDocsourcePayload(id, {
+ id: 'foo',
+ version: 'foo',
+ attributes: {
+ title: 'something',
+ },
+ });
+
const indexPattern = await indexPatterns.get(id);
expect(indexPattern).toBeDefined();
@@ -107,4 +132,41 @@ describe('IndexPatterns', () => {
await indexPatterns.delete(id);
expect(indexPattern).not.toBe(await indexPatterns.get(id));
});
+
+ test('should handle version conflicts', async () => {
+ setDocsourcePayload(null, {
+ id: 'foo',
+ version: 'foo',
+ attributes: {
+ title: 'something',
+ },
+ });
+
+ // Create a normal index patterns
+ const pattern = await indexPatterns.make('foo');
+
+ expect(pattern.version).toBe('fooa');
+
+ // Create the same one - we're going to handle concurrency
+ const samePattern = await indexPatterns.make('foo');
+
+ expect(samePattern.version).toBe('fooaa');
+
+ // This will conflict because samePattern did a save (from refreshFields)
+ // but the resave should work fine
+ pattern.title = 'foo2';
+ await indexPatterns.save(pattern);
+
+ // This should not be able to recover
+ samePattern.title = 'foo3';
+
+ let result;
+ try {
+ await indexPatterns.save(samePattern);
+ } catch (err) {
+ result = err;
+ }
+
+ expect(result.res.status).toBe(409);
+ });
});
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index fe0d14b2d9c19..88a7e9f6cef4c 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { i18n } from '@kbn/i18n';
import { SavedObjectsClientCommon } from '../..';
import { createIndexPatternCache } from '.';
@@ -37,6 +38,8 @@ import { FieldFormatsStartCommon } from '../../field_formats';
import { UI_SETTINGS, SavedObject } from '../../../common';
const indexPatternCache = createIndexPatternCache();
+const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
+const savedObjectType = 'index-pattern';
type IndexPatternCachedFieldType = 'id' | 'title';
@@ -181,6 +184,7 @@ export class IndexPatternsService {
apiClient: this.apiClient,
patternCache: indexPatternCache,
fieldFormats: this.fieldFormats,
+ indexPatternsService: this,
onNotification: this.onNotification,
onError: this.onError,
shortDotsEnable,
@@ -191,6 +195,93 @@ export class IndexPatternsService {
return indexPattern;
}
+ async save(indexPattern: IndexPattern, saveAttempts: number = 0): Promise {
+ if (!indexPattern.id) return;
+ const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
+ const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
+
+ const body = indexPattern.prepBody();
+
+ const originalChangedKeys: string[] = [];
+ Object.entries(body).forEach(([key, value]) => {
+ if (value !== indexPattern.originalBody[key]) {
+ originalChangedKeys.push(key);
+ }
+ });
+
+ return this.savedObjectsClient
+ .update(savedObjectType, indexPattern.id, body, { version: indexPattern.version })
+ .then((resp) => {
+ indexPattern.id = resp.id;
+ indexPattern.version = resp.version;
+ })
+ .catch((err) => {
+ if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) {
+ const samePattern = new IndexPattern(indexPattern.id, {
+ savedObjectsClient: this.savedObjectsClient,
+ apiClient: this.apiClient,
+ patternCache: indexPatternCache,
+ fieldFormats: this.fieldFormats,
+ indexPatternsService: this,
+ onNotification: this.onNotification,
+ onError: this.onError,
+ shortDotsEnable,
+ metaFields,
+ });
+
+ return samePattern.init().then(() => {
+ // What keys changed from now and what the server returned
+ const updatedBody = samePattern.prepBody();
+
+ // Build a list of changed keys from the server response
+ // and ensure we ignore the key if the server response
+ // is the same as the original response (since that is expected
+ // if we made a change in that key)
+
+ const serverChangedKeys: string[] = [];
+ Object.entries(updatedBody).forEach(([key, value]) => {
+ if (value !== (body as any)[key] && value !== indexPattern.originalBody[key]) {
+ serverChangedKeys.push(key);
+ }
+ });
+
+ let unresolvedCollision = false;
+ for (const originalKey of originalChangedKeys) {
+ for (const serverKey of serverChangedKeys) {
+ if (originalKey === serverKey) {
+ unresolvedCollision = true;
+ break;
+ }
+ }
+ }
+
+ if (unresolvedCollision) {
+ const title = i18n.translate('data.indexPatterns.unableWriteLabel', {
+ defaultMessage:
+ 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.',
+ });
+
+ this.onNotification({ title, color: 'danger' });
+ throw err;
+ }
+
+ // Set the updated response on this object
+ serverChangedKeys.forEach((key) => {
+ (indexPattern as any)[key] = (samePattern as any)[key];
+ });
+ indexPattern.version = samePattern.version;
+
+ // Clear cache
+ indexPatternCache.clear(indexPattern.id!);
+
+ // Try the save again
+ return this.save(indexPattern, saveAttempts);
+ });
+ }
+ throw err;
+ });
+ }
+
async make(id?: string): Promise {
const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
@@ -200,6 +291,7 @@ export class IndexPatternsService {
apiClient: this.apiClient,
patternCache: indexPatternCache,
fieldFormats: this.fieldFormats,
+ indexPatternsService: this,
onNotification: this.onNotification,
onError: this.onError,
shortDotsEnable,
diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts
index 54757b53b8665..d8f7b5091eb8f 100644
--- a/src/plugins/data/common/search/es_search/index.ts
+++ b/src/plugins/data/common/search/es_search/index.ts
@@ -17,10 +17,4 @@
* under the License.
*/
-export {
- ISearchRequestParams,
- IEsSearchRequest,
- IEsSearchResponse,
- ES_SEARCH_STRATEGY,
- ISearchOptions,
-} from './types';
+export * from './types';
diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts
index 89faa5b7119c8..81124c1e095f7 100644
--- a/src/plugins/data/common/search/es_search/types.ts
+++ b/src/plugins/data/common/search/es_search/types.ts
@@ -53,3 +53,6 @@ export interface IEsSearchResponse
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
index 9ad82723c1161..0a330d074fd42 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
@@ -122,6 +122,8 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
grow={false}
>
@@ -420,6 +422,8 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
grow={false}
>
@@ -553,7 +557,7 @@ exports[`Flyout should render import step 1`] = `
hasEmptyLabelSpace={false}
label={
@@ -603,6 +607,8 @@ exports[`Flyout should render import step 1`] = `
grow={false}
>
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap
index 642a5030e4ec0..038e1aaf2d8f5 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap
@@ -92,7 +92,7 @@ exports[`Header should render normally 1`] = `
color="subdued"
>
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx
index 32462e1e2184d..cc9d2ed160241 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx
@@ -267,6 +267,10 @@ describe('Flyout', () => {
expect(component.state('status')).toBe('success');
expect(component.find('EuiFlyout ImportSummary')).toMatchSnapshot();
+ const cancelButton = await component.find(
+ 'EuiButtonEmpty[data-test-subj="importSavedObjectsCancelBtn"]'
+ );
+ expect(cancelButton.prop('disabled')).toBe(true);
});
});
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx
index eddca18f9e283..47d82077294cc 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx
@@ -729,7 +729,7 @@ export class Flyout extends Component {
label={
}
>
@@ -756,7 +756,7 @@ export class Flyout extends Component {
}
renderFooter() {
- const { status } = this.state;
+ const { isLegacyFile, status } = this.state;
const { done, close } = this.props;
let confirmButton;
@@ -773,7 +773,7 @@ export class Flyout extends Component {
} else if (this.hasUnmatchedReferences) {
confirmButton = (
{
} else {
confirmButton = (
{
return (
-
+
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx
index ac8099893d00e..4000d620465a8 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx
@@ -51,8 +51,7 @@ const createNewCopiesDisabled = {
tooltip: i18n.translate(
'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledText',
{
- defaultMessage:
- 'Check if each object was previously copied or imported into the destination space.',
+ defaultMessage: 'Check if objects were previously copied or imported.',
}
),
};
@@ -64,21 +63,23 @@ const createNewCopiesEnabled = {
),
tooltip: i18n.translate(
'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText',
- { defaultMessage: 'All imported objects will be created with new random IDs.' }
+ {
+ defaultMessage: 'Use this option to create one or more copies of the object.',
+ }
),
};
const overwriteEnabled = {
id: 'overwriteEnabled',
label: i18n.translate(
'savedObjectsManagement.objectsTable.importModeControl.overwrite.enabledLabel',
- { defaultMessage: 'Automatically try to overwrite conflicts' }
+ { defaultMessage: 'Automatically overwrite conflicts' }
),
};
const overwriteDisabled = {
id: 'overwriteDisabled',
label: i18n.translate(
'savedObjectsManagement.objectsTable.importModeControl.overwrite.disabledLabel',
- { defaultMessage: 'Request action when conflict occurs' }
+ { defaultMessage: 'Request action on conflict' }
),
};
const importOptionsTitle = i18n.translate(
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx
index ed65131b0fc6b..20ac5a903ef22 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx
@@ -70,7 +70,7 @@ describe('ImportSummary', () => {
const wrapper = shallowWithI18nProvider();
expect(findHeader(wrapper).childAt(0).props()).toEqual(
- expect.objectContaining({ values: { importCount: 1 } })
+ expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
);
const countCreated = findCountCreated(wrapper);
expect(countCreated).toHaveLength(1);
@@ -90,7 +90,7 @@ describe('ImportSummary', () => {
const wrapper = shallowWithI18nProvider();
expect(findHeader(wrapper).childAt(0).props()).toEqual(
- expect.objectContaining({ values: { importCount: 1 } })
+ expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
);
expect(findCountCreated(wrapper)).toHaveLength(0);
const countOverwritten = findCountOverwritten(wrapper);
@@ -110,7 +110,7 @@ describe('ImportSummary', () => {
const wrapper = shallowWithI18nProvider();
expect(findHeader(wrapper).childAt(0).props()).toEqual(
- expect.objectContaining({ values: { importCount: 1 } })
+ expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
);
expect(findCountCreated(wrapper)).toHaveLength(0);
expect(findCountOverwritten(wrapper)).toHaveLength(0);
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx
index 7949f7d18d350..e2ce3c3695b17 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx
@@ -141,7 +141,7 @@ const getCountIndicators = (importItems: ImportItem[]) => {
);
};
-const getStatusIndicator = ({ outcome, errorMessage }: ImportItem) => {
+const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) => {
switch (outcome) {
case 'created':
return (
@@ -168,8 +168,8 @@ const getStatusIndicator = ({ outcome, errorMessage }: ImportItem) => {
type={'alert'}
color={'danger'}
content={i18n.translate('savedObjectsManagement.importSummary.errorOutcomeLabel', {
- defaultMessage: 'Error{message}',
- values: { message: errorMessage ? `: ${errorMessage}` : '' },
+ defaultMessage: '{errorMessage}',
+ values: { errorMessage },
})}
/>
);
@@ -194,11 +194,18 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar
}
>
-
+ {importItems.length === 1 ? (
+
+ ) : (
+
+ )}
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx
index c93bc9e5038df..7576b62552aa2 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx
@@ -40,7 +40,7 @@ describe('OverwriteModal', () => {
const wrapper = shallowWithI18nProvider();
expect(wrapper.find('p').text()).toMatchInlineSnapshot(
- `"\\"baz\\" conflicts with an existing object, are you sure you want to overwrite it?"`
+ `"\\"baz\\" conflicts with an existing object. Overwrite it?"`
);
expect(wrapper.find('EuiSuperSelect')).toHaveLength(0);
});
@@ -82,7 +82,7 @@ describe('OverwriteModal', () => {
const wrapper = shallowWithI18nProvider();
expect(wrapper.find('p').text()).toMatchInlineSnapshot(
- `"\\"baz\\" conflicts with multiple existing objects, do you want to overwrite one of them?"`
+ `"\\"baz\\" conflicts with multiple existing objects. Overwrite one?"`
);
expect(wrapper.find('EuiSuperSelect')).toHaveLength(1);
});
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx
index dbe95161cbeae..bf27d407fbe94 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx
@@ -98,15 +98,13 @@ export const OverwriteModal = ({ conflict, onFinish }: OverwriteModalProps) => {
const bodyText =
error.type === 'conflict'
? i18n.translate('savedObjectsManagement.objectsTable.overwriteModal.body.conflict', {
- defaultMessage:
- '"{title}" conflicts with an existing object, are you sure you want to overwrite it?',
+ defaultMessage: '"{title}" conflicts with an existing object. Overwrite it?',
values: { title },
})
: i18n.translate(
'savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict',
{
- defaultMessage:
- '"{title}" conflicts with multiple existing objects, do you want to overwrite one of them?',
+ defaultMessage: '"{title}" conflicts with multiple existing objects. Overwrite one?',
values: { title },
}
);
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index acd575badbe5b..5bce03a292760 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -414,6 +414,34 @@
}
}
},
+ "enterpriseSearch": {
+ "properties": {
+ "clicks_total": {
+ "type": "long"
+ },
+ "clicks_7_days": {
+ "type": "long"
+ },
+ "clicks_30_days": {
+ "type": "long"
+ },
+ "clicks_90_days": {
+ "type": "long"
+ },
+ "minutes_on_screen_total": {
+ "type": "float"
+ },
+ "minutes_on_screen_7_days": {
+ "type": "float"
+ },
+ "minutes_on_screen_30_days": {
+ "type": "float"
+ },
+ "minutes_on_screen_90_days": {
+ "type": "float"
+ }
+ }
+ },
"appSearch": {
"properties": {
"clicks_total": {
diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts
index fe77ebeb0866d..d5a5ec4640d4b 100644
--- a/src/plugins/timelion/server/plugin.ts
+++ b/src/plugins/timelion/server/plugin.ts
@@ -42,7 +42,7 @@ const showWarningMessageIfTimelionSheetWasFound = (core: CoreStart, logger: Logg
({ total }) =>
total &&
logger.warn(
- 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation.'
+ 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/master/dashboard.html#timelion-deprecation.'
)
);
};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
index f969778bbc615..34f339ce24c21 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
@@ -54,6 +54,26 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig
};
set(variables, varName, data);
set(variables, `${_.snakeCase(row.label)}.label`, row.label);
+
+ /**
+ * Handle the case when a field has "key_as_string" value.
+ * Common case is the value is a date string (e.x. "2020-08-21T20:36:58.000Z") or a boolean stringified value ("true"/"false").
+ * Try to convert the value into a moment object and format it with "dateFormat" from UI settings,
+ * if the "key_as_string" value is recognized by a known format in Moments.js https://momentjs.com/docs/#/parsing/string/ .
+ * If not, return a formatted value from elasticsearch
+ */
+ if (row.labelFormatted) {
+ const momemntObj = moment(row.labelFormatted);
+ let val;
+
+ if (momemntObj.isValid()) {
+ val = momemntObj.format(dateFormat);
+ } else {
+ val = row.labelFormatted;
+ }
+
+ set(variables, `${_.snakeCase(row.label)}.formatted`, val);
+ }
});
});
return variables;
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
index 54139a7c27e3f..37cc7fd3380d0 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
@@ -42,6 +42,7 @@ export function getSplits(resp, panel, series, meta) {
return buckets.map((bucket) => {
bucket.id = `${series.id}:${bucket.key}`;
bucket.label = formatKey(bucket.key, series);
+ bucket.labelFormatted = bucket.key_as_string || '';
bucket.color = panel.type === 'top_n' ? color.string() : colors.shift();
bucket.meta = meta;
return bucket;
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js
index 376d32d0da13f..28f056613b082 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js
@@ -89,6 +89,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-01',
key: 'example-01',
label: 'example-01',
+ labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
@@ -98,6 +99,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-02',
key: 'example-02',
label: 'example-02',
+ labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
@@ -145,6 +147,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-01',
key: 'example-01',
label: 'example-01',
+ labelFormatted: '',
meta: { bucketSize: 10 },
color: undefined,
timeseries: { buckets: [] },
@@ -154,6 +157,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-02',
key: 'example-02',
label: 'example-02',
+ labelFormatted: '',
meta: { bucketSize: 10 },
color: undefined,
timeseries: { buckets: [] },
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js
index 0d567b7fd4154..e04c3a93e81bb 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js
@@ -40,6 +40,7 @@ export function stdMetric(resp, panel, series, meta) {
results.push({
id: `${split.id}`,
label: split.label,
+ labelFormatted: split.labelFormatted,
color: split.color,
data,
...decoration,
diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts
index 8a1541ecae0d4..4ea25af549249 100644
--- a/src/plugins/vis_type_vega/public/data_model/search_api.ts
+++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts
@@ -51,9 +51,6 @@ export class SearchAPI {
searchRequests.map((request) => {
const requestId = request.name;
const params = getSearchParamsFromRequest(request, {
- esShardTimeout: this.dependencies.injectedMetadata.getInjectedVar(
- 'esShardTimeout'
- ) as number,
getConfig: this.dependencies.uiSettings.get.bind(this.dependencies.uiSettings),
});
diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts
index 00c6b2e3c8d5b..4b8ff8e2cb43a 100644
--- a/src/plugins/vis_type_vega/public/plugin.ts
+++ b/src/plugins/vis_type_vega/public/plugin.ts
@@ -78,7 +78,6 @@ export class VegaPlugin implements Plugin, void> {
) {
setInjectedVars({
enableExternalUrls: this.initializerContext.config.get().enableExternalUrls,
- esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number,
emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true),
});
setUISettings(core.uiSettings);
diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts
index acd02a6dd42f8..dfb2c96e9f894 100644
--- a/src/plugins/vis_type_vega/public/services.ts
+++ b/src/plugins/vis_type_vega/public/services.ts
@@ -48,7 +48,6 @@ export const [getSavedObjects, setSavedObjects] = createGetterSetter('InjectedVars');
diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js
index 0912edf9503a6..1bf625af76207 100644
--- a/src/plugins/vis_type_vega/public/vega_visualization.test.js
+++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js
@@ -82,7 +82,6 @@ describe('VegaVisualizations', () => {
setInjectedVars({
emsTileLayerId: {},
enableExternalUrls: true,
- esShardTimeout: 10000,
});
setData(dataPluginStart);
setSavedObjects(coreStart.savedObjects);
diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts
index 9997d9710e212..99a58620b17f5 100644
--- a/test/api_integration/apis/saved_objects/migrations.ts
+++ b/test/api_integration/apis/saved_objects/migrations.ts
@@ -379,14 +379,12 @@ async function migrateIndex({
index,
migrations,
mappingProperties,
- validateDoc,
obsoleteIndexTemplatePattern,
}: {
esClient: ElasticsearchClient;
index: string;
migrations: Record;
mappingProperties: SavedObjectsTypeMappingDefinitions;
- validateDoc?: (doc: any) => void;
obsoleteIndexTemplatePattern?: string;
}) {
const typeRegistry = new SavedObjectTypeRegistry();
@@ -396,7 +394,6 @@ async function migrateIndex({
const documentMigrator = new DocumentMigrator({
kibanaVersion: '99.9.9',
typeRegistry,
- validateDoc: validateDoc || _.noop,
log: getLogMock(),
});
diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts
index d6a4fdd67b0a1..1c85f226623cb 100644
--- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts
+++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts
@@ -78,7 +78,7 @@ export class IndexPatternsTestPlugin
const id = (req.params as Record).id;
const service = await data.indexPatterns.indexPatternsServiceFactory(req);
const ip = await service.get(id);
- await ip.save();
+ await service.save(ip);
return res.ok();
}
);
diff --git a/x-pack/package.json b/x-pack/package.json
index 899eca1095923..3a074ba1f1d7d 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -195,7 +195,7 @@
"jsdom": "13.1.0",
"jsondiffpatch": "0.4.1",
"jsts": "^1.6.2",
- "kea": "2.2.0-rc.4",
+ "kea": "^2.2.0",
"loader-utils": "^1.2.3",
"lz-string": "^1.4.4",
"madge": "3.4.4",
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 868f6f180cc91..3bc8acead6c13 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -19,7 +19,7 @@ Table of Contents
- [Usage](#usage)
- [Kibana Actions Configuration](#kibana-actions-configuration)
- [Configuration Options](#configuration-options)
- - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-hosts-allow-list)
+ - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts)
- [Configuration Utilities](#configuration-utilities)
- [Action types](#action-types)
- [Methods](#methods)
@@ -74,13 +74,21 @@ Table of Contents
- [`secrets`](#secrets-7)
- [`params`](#params-7)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
+ - [`subActionParams (issueTypes)`](#subactionparams-issuetypes)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
- [IBM Resilient](#ibm-resilient)
- [`config`](#config-8)
- [`secrets`](#secrets-8)
- [`params`](#params-8)
- - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
+ - [licensing](#licensing)
+ - [plugin location](#plugin-location)
+ - [documentation](#documentation)
+ - [tests](#tests)
+ - [action type config and secrets](#action-type-config-and-secrets)
+ - [user interface](#user-interface)
## Terminology
@@ -103,12 +111,12 @@ Implemented under the [Actions Config](./server/actions_config.ts).
Built-In-Actions are configured using the _xpack.actions_ namespoace under _kibana.yml_, and have the following configuration options:
-| Namespaced Key | Description | Type |
-| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
-| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean |
-| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array |
-| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array |
-| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array
diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
index 24b51e3fba917..9706895b164a6 100644
--- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
@@ -18,6 +18,7 @@ exports[`Home component should render services 1`] = `
"currentAppId$": Observable {
"_isScalar": false,
},
+ "navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
@@ -78,6 +79,7 @@ exports[`Home component should render traces 1`] = `
"currentAppId$": Observable {
"_isScalar": false,
},
+ "navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
diff --git a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx
deleted file mode 100644
index bf1cd75432ff5..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import { BreadcrumbRoute, getBreadcrumbs } from './ProvideBreadcrumbs';
-import { RouteName } from './route_config/route_names';
-
-describe('getBreadcrumbs', () => {
- const getTestRoutes = (): BreadcrumbRoute[] => [
- { path: '/a', exact: true, breadcrumb: 'A', name: RouteName.HOME },
- {
- path: '/a/ignored',
- exact: true,
- breadcrumb: 'Ignored Route',
- name: RouteName.METRICS,
- },
- {
- path: '/a/:letter',
- exact: true,
- name: RouteName.SERVICE,
- breadcrumb: ({ match }) => `Second level: ${match.params.letter}`,
- },
- {
- path: '/a/:letter/c',
- exact: true,
- name: RouteName.ERRORS,
- breadcrumb: ({ match }) => `Third level: ${match.params.letter}`,
- },
- ];
-
- const getLocation = () =>
- ({
- pathname: '/a/b/c/',
- } as Location);
-
- it('should return a set of matching breadcrumbs for a given path', () => {
- const breadcrumbs = getBreadcrumbs({
- location: getLocation(),
- routes: getTestRoutes(),
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Second level: b",
- "Third level: b",
-]
-`);
- });
-
- it('should skip breadcrumbs if breadcrumb is null', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- routes[2].breadcrumb = null;
-
- const breadcrumbs = getBreadcrumbs({
- location,
- routes,
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Third level: b",
-]
-`);
- });
-
- it('should skip breadcrumbs if breadcrumb key is missing', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- // @ts-expect-error
- delete routes[2].breadcrumb;
-
- const breadcrumbs = getBreadcrumbs({ location, routes });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Third level: b",
-]
-`);
- });
-
- it('should produce matching breadcrumbs even if the pathname has a query string appended', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- location.pathname += '?some=thing';
-
- const breadcrumbs = getBreadcrumbs({
- location,
- routes,
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Second level: b",
- "Third level: b",
-]
-`);
- });
-});
diff --git a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx
deleted file mode 100644
index f2505b64fb1e3..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import React from 'react';
-import {
- matchPath,
- RouteComponentProps,
- RouteProps,
- withRouter,
-} from 'react-router-dom';
-import { RouteName } from './route_config/route_names';
-
-type LocationMatch = Pick<
- RouteComponentProps>,
- 'location' | 'match'
->;
-
-type BreadcrumbFunction = (props: LocationMatch) => string;
-
-export interface BreadcrumbRoute extends RouteProps {
- breadcrumb: string | BreadcrumbFunction | null;
- name: RouteName;
-}
-
-export interface Breadcrumb extends LocationMatch {
- value: string;
-}
-
-interface RenderProps extends RouteComponentProps {
- breadcrumbs: Breadcrumb[];
-}
-
-interface ProvideBreadcrumbsProps extends RouteComponentProps {
- routes: BreadcrumbRoute[];
- render: (props: RenderProps) => React.ReactElement | null;
-}
-
-interface ParseOptions extends LocationMatch {
- breadcrumb: string | BreadcrumbFunction;
-}
-
-const parse = (options: ParseOptions) => {
- const { breadcrumb, match, location } = options;
- let value;
-
- if (typeof breadcrumb === 'function') {
- value = breadcrumb({ match, location });
- } else {
- value = breadcrumb;
- }
-
- return { value, match, location };
-};
-
-export function getBreadcrumb({
- location,
- currentPath,
- routes,
-}: {
- location: Location;
- currentPath: string;
- routes: BreadcrumbRoute[];
-}) {
- return routes.reduce((found, { breadcrumb, ...route }) => {
- if (found) {
- return found;
- }
-
- if (!breadcrumb) {
- return null;
- }
-
- const match = matchPath>(currentPath, route);
-
- if (match) {
- return parse({
- breadcrumb,
- match,
- location,
- });
- }
-
- return null;
- }, null);
-}
-
-export function getBreadcrumbs({
- routes,
- location,
-}: {
- routes: BreadcrumbRoute[];
- location: Location;
-}) {
- const breadcrumbs: Breadcrumb[] = [];
- const { pathname } = location;
-
- pathname
- .split('?')[0]
- .replace(/\/$/, '')
- .split('/')
- .reduce((acc, next) => {
- // `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
- const currentPath = !next ? '/' : `${acc}/${next}`;
- const breadcrumb = getBreadcrumb({
- location,
- currentPath,
- routes,
- });
-
- if (breadcrumb) {
- breadcrumbs.push(breadcrumb);
- }
-
- return currentPath === '/' ? '' : currentPath;
- }, '');
-
- return breadcrumbs;
-}
-
-function ProvideBreadcrumbsComponent({
- routes = [],
- render,
- location,
- match,
- history,
-}: ProvideBreadcrumbsProps) {
- const breadcrumbs = getBreadcrumbs({ routes, location });
- return render({ breadcrumbs, location, match, history });
-}
-
-export const ProvideBreadcrumbs = withRouter(ProvideBreadcrumbsComponent);
diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
deleted file mode 100644
index 5bf5cea587f93..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import React, { MouseEvent } from 'react';
-import { CoreStart } from 'src/core/public';
-import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
-import { getAPMHref } from '../../shared/Links/apm/APMLink';
-import {
- Breadcrumb,
- BreadcrumbRoute,
- ProvideBreadcrumbs,
-} from './ProvideBreadcrumbs';
-
-interface Props {
- location: Location;
- breadcrumbs: Breadcrumb[];
- core: CoreStart;
-}
-
-function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
- return breadcrumbs.map(({ value }) => value).reverse();
-}
-
-class UpdateBreadcrumbsComponent extends React.Component {
- public updateHeaderBreadcrumbs() {
- const { basePath } = this.props.core.http;
- const breadcrumbs = this.props.breadcrumbs.map(
- ({ value, match }, index) => {
- const { search } = this.props.location;
- const isLastBreadcrumbItem =
- index === this.props.breadcrumbs.length - 1;
- const href = isLastBreadcrumbItem
- ? undefined // makes the breadcrumb item not clickable
- : getAPMHref({ basePath, path: match.url, search });
- return {
- text: value,
- href,
- onClick: (event: MouseEvent) => {
- if (href) {
- event.preventDefault();
- this.props.core.application.navigateToUrl(href);
- }
- },
- };
- }
- );
-
- this.props.core.chrome.docTitle.change(
- getTitleFromBreadCrumbs(this.props.breadcrumbs)
- );
- this.props.core.chrome.setBreadcrumbs(breadcrumbs);
- }
-
- public componentDidMount() {
- this.updateHeaderBreadcrumbs();
- }
-
- public componentDidUpdate() {
- this.updateHeaderBreadcrumbs();
- }
-
- public render() {
- return null;
- }
-}
-
-interface UpdateBreadcrumbsProps {
- routes: BreadcrumbRoute[];
-}
-
-export function UpdateBreadcrumbs({ routes }: UpdateBreadcrumbsProps) {
- const { core } = useApmPluginContext();
-
- return (
- (
-
- )}
- />
- );
-}
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
index 56026dcf477ec..0cefcbdc54228 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
@@ -7,38 +7,32 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
+import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
+import { APMRouteDefinition } from '../../../../application/routes';
+import { toQuery } from '../../../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
-import { ServiceDetails } from '../../ServiceDetails';
-import { TransactionDetails } from '../../TransactionDetails';
import { Home } from '../../Home';
-import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
-import { RouteName } from './route_names';
+import { ServiceDetails } from '../../ServiceDetails';
+import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
import { Settings } from '../../Settings';
import { AgentConfigurations } from '../../Settings/AgentConfigurations';
+import { AnomalyDetection } from '../../Settings/anomaly_detection';
import { ApmIndices } from '../../Settings/ApmIndices';
-import { toQuery } from '../../../shared/Links/url_helpers';
-import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
-import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams';
-import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
-import { TraceLink } from '../../TraceLink';
import { CustomizeUI } from '../../Settings/CustomizeUI';
-import { AnomalyDetection } from '../../Settings/anomaly_detection';
+import { TraceLink } from '../../TraceLink';
+import { TransactionDetails } from '../../TransactionDetails';
import {
CreateAgentConfigurationRouteHandler,
EditAgentConfigurationRouteHandler,
} from './route_handlers/agent_configuration';
-const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
- defaultMessage: 'Metrics',
-});
-
-interface RouteParams {
- serviceName: string;
-}
-
-export const renderAsRedirectTo = (to: string) => {
- return ({ location }: RouteComponentProps) => {
+/**
+ * Given a path, redirect to that location, preserving the search and maintaining
+ * backward-compatibilty with legacy (pre-7.9) hash-based URLs.
+ */
+export function renderAsRedirectTo(to: string) {
+ return ({ location }: RouteComponentProps<{}>) => {
let resolvedUrl: URL | undefined;
// Redirect root URLs with a hash to support backward compatibility with URLs
@@ -60,71 +54,149 @@ export const renderAsRedirectTo = (to: string) => {
/>
);
};
-};
+}
+
+// These component function definitions are used below with the `component`
+// property of the route definitions.
+//
+// If you provide an inline function to the component prop, you would create a
+// new component every render. This results in the existing component unmounting
+// and the new component mounting instead of just updating the existing component.
+//
+// This means you should use `render` if you're providing an inline function.
+// However, the `ApmRoute` component from @elastic/apm-rum-react, only supports
+// `component`, and will give you a large console warning if you use `render`.
+//
+// This warning cannot be turned off
+// (see https://github.com/elastic/apm-agent-rum-js/issues/881) so while this is
+// slightly more code, it provides better performance without causing console
+// warnings to appear.
+function HomeServices() {
+ return ;
+}
+
+function HomeServiceMap() {
+ return ;
+}
+
+function HomeTraces() {
+ return ;
+}
+
+function ServiceDetailsErrors(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
-export const routes: BreadcrumbRoute[] = [
+function ServiceDetailsMetrics(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsNodes(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsServiceMap(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsTransactions(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function SettingsAgentConfiguration() {
+ return (
+
+
+
+ );
+}
+
+function SettingsAnomalyDetection() {
+ return (
+
+
+
+ );
+}
+
+function SettingsApmIndices() {
+ return (
+
+
+
+ );
+}
+
+function SettingsCustomizeUI() {
+ return (
+
+
+
+ );
+}
+
+/**
+ * The array of route definitions to be used when the application
+ * creates the routes.
+ */
+export const routes: APMRouteDefinition[] = [
{
exact: true,
path: '/',
- render: renderAsRedirectTo('/services'),
+ component: renderAsRedirectTo('/services'),
breadcrumb: 'APM',
- name: RouteName.HOME,
},
{
exact: true,
path: '/services',
- component: () => ,
+ component: HomeServices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
defaultMessage: 'Services',
}),
- name: RouteName.SERVICES,
},
{
exact: true,
path: '/traces',
- component: () => ,
+ component: HomeTraces,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
defaultMessage: 'Traces',
}),
- name: RouteName.TRACES,
},
{
exact: true,
path: '/settings',
- render: renderAsRedirectTo('/settings/agent-configuration'),
+ component: renderAsRedirectTo('/settings/agent-configuration'),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
defaultMessage: 'Settings',
}),
- name: RouteName.SETTINGS,
},
{
exact: true,
path: '/settings/apm-indices',
- component: () => (
-
-
-
- ),
+ component: SettingsApmIndices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', {
defaultMessage: 'Indices',
}),
- name: RouteName.INDICES,
},
{
exact: true,
path: '/settings/agent-configuration',
- component: () => (
-
-
-
- ),
+ component: SettingsAgentConfiguration,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.agentConfigurationTitle',
{ defaultMessage: 'Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION,
},
-
{
exact: true,
path: '/settings/agent-configuration/create',
@@ -132,8 +204,7 @@ export const routes: BreadcrumbRoute[] = [
'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle',
{ defaultMessage: 'Create Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION_CREATE,
- component: () => ,
+ component: CreateAgentConfigurationRouteHandler,
},
{
exact: true,
@@ -142,71 +213,66 @@ export const routes: BreadcrumbRoute[] = [
'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle',
{ defaultMessage: 'Edit Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION_EDIT,
- component: () => ,
+ component: EditAgentConfigurationRouteHandler,
},
{
exact: true,
path: '/services/:serviceName',
breadcrumb: ({ match }) => match.params.serviceName,
- render: (props: RouteComponentProps) =>
+ component: (props: RouteComponentProps<{ serviceName: string }>) =>
renderAsRedirectTo(
`/services/${props.match.params.serviceName}/transactions`
)(props),
- name: RouteName.SERVICE,
- },
+ } as APMRouteDefinition<{ serviceName: string }>,
// errors
{
exact: true,
path: '/services/:serviceName/errors/:groupId',
component: ErrorGroupDetails,
breadcrumb: ({ match }) => match.params.groupId,
- name: RouteName.ERROR,
- },
+ } as APMRouteDefinition<{ groupId: string; serviceName: string }>,
{
exact: true,
path: '/services/:serviceName/errors',
- component: () => ,
+ component: ServiceDetailsErrors,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
defaultMessage: 'Errors',
}),
- name: RouteName.ERRORS,
},
// transactions
{
exact: true,
path: '/services/:serviceName/transactions',
- component: () => ,
+ component: ServiceDetailsTransactions,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
defaultMessage: 'Transactions',
}),
- name: RouteName.TRANSACTIONS,
},
// metrics
{
exact: true,
path: '/services/:serviceName/metrics',
- component: () => ,
- breadcrumb: metricsBreadcrumb,
- name: RouteName.METRICS,
+ component: ServiceDetailsMetrics,
+ breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
+ defaultMessage: 'Metrics',
+ }),
},
// service nodes, only enabled for java agents for now
{
exact: true,
path: '/services/:serviceName/nodes',
- component: () => ,
+ component: ServiceDetailsNodes,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', {
defaultMessage: 'JVMs',
}),
- name: RouteName.SERVICE_NODES,
},
// node metrics
{
exact: true,
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
- component: () => ,
- breadcrumb: ({ location }) => {
- const { serviceNodeName } = resolveUrlParams(location, {});
+ component: ServiceNodeMetrics,
+ breadcrumb: ({ match }) => {
+ const { serviceNodeName } = match.params;
if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
return UNIDENTIFIED_SERVICE_NODES_LABEL;
@@ -214,7 +280,6 @@ export const routes: BreadcrumbRoute[] = [
return serviceNodeName || '';
},
- name: RouteName.SERVICE_NODE_METRICS,
},
{
exact: true,
@@ -224,61 +289,46 @@ export const routes: BreadcrumbRoute[] = [
const query = toQuery(location.search);
return query.transactionName as string;
},
- name: RouteName.TRANSACTION_NAME,
},
{
exact: true,
path: '/link-to/trace/:traceId',
component: TraceLink,
breadcrumb: null,
- name: RouteName.LINK_TO_TRACE,
},
-
{
exact: true,
path: '/service-map',
- component: () => ,
+ component: HomeServiceMap,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
- name: RouteName.SERVICE_MAP,
},
{
exact: true,
path: '/services/:serviceName/service-map',
- component: () => ,
+ component: ServiceDetailsServiceMap,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
- name: RouteName.SINGLE_SERVICE_MAP,
},
{
exact: true,
path: '/settings/customize-ui',
- component: () => (
-
-
-
- ),
+ component: SettingsCustomizeUI,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', {
defaultMessage: 'Customize UI',
}),
- name: RouteName.CUSTOMIZE_UI,
},
{
exact: true,
path: '/settings/anomaly-detection',
- component: () => (
-
-
-
- ),
+ component: SettingsAnomalyDetection,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
- name: RouteName.ANOMALY_DETECTION,
},
];
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
index ad12afe35fa20..21a162111bc79 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
@@ -14,7 +14,7 @@ describe('routes', () => {
it('redirects to /services', () => {
const location = { hash: '', pathname: '/', search: '' };
expect(
- (route as any).render({ location } as any).props.to.pathname
+ (route as any).component({ location } as any).props.to.pathname
).toEqual('/services');
});
});
@@ -28,7 +28,9 @@ describe('routes', () => {
search: '',
};
- expect(((route as any).render({ location }) as any).props.to).toEqual({
+ expect(
+ ((route as any).component({ location }) as any).props.to
+ ).toEqual({
hash: '',
pathname: '/services/opbeans-python/transactions/view',
search:
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
index 7a00840daa3c5..cc07286457908 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
@@ -5,14 +5,17 @@
*/
import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { RouteComponentProps } from 'react-router-dom';
import { useFetcher } from '../../../../../hooks/useFetcher';
import { toQuery } from '../../../../shared/Links/url_helpers';
import { Settings } from '../../../Settings';
import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit';
-export function EditAgentConfigurationRouteHandler() {
- const history = useHistory();
+type EditAgentConfigurationRouteHandler = RouteComponentProps<{}>;
+
+export function EditAgentConfigurationRouteHandler({
+ history,
+}: EditAgentConfigurationRouteHandler) {
const { search } = history.location;
// typescript complains because `pageStop` does not exist in `APMQueryParams`
@@ -40,8 +43,11 @@ export function EditAgentConfigurationRouteHandler() {
);
}
-export function CreateAgentConfigurationRouteHandler() {
- const history = useHistory();
+type CreateAgentConfigurationRouteHandlerProps = RouteComponentProps<{}>;
+
+export function CreateAgentConfigurationRouteHandler({
+ history,
+}: CreateAgentConfigurationRouteHandlerProps) {
const { search } = history.location;
// Ignoring here because we specifically DO NOT want to add the query params to the global route handler
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
deleted file mode 100644
index 1bf798e3b26d7..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export enum RouteName {
- HOME = 'home',
- SERVICES = 'services',
- SERVICE_MAP = 'service-map',
- SINGLE_SERVICE_MAP = 'single-service-map',
- TRACES = 'traces',
- SERVICE = 'service',
- TRANSACTIONS = 'transactions',
- ERRORS = 'errors',
- ERROR = 'error',
- METRICS = 'metrics',
- SERVICE_NODE_METRICS = 'node_metrics',
- TRANSACTION_TYPE = 'transaction_type',
- TRANSACTION_NAME = 'transaction_name',
- SETTINGS = 'settings',
- AGENT_CONFIGURATION = 'agent_configuration',
- AGENT_CONFIGURATION_CREATE = 'agent_configuration_create',
- AGENT_CONFIGURATION_EDIT = 'agent_configuration_edit',
- INDICES = 'indices',
- SERVICE_NODES = 'nodes',
- LINK_TO_TRACE = 'link_to_trace',
- CUSTOMIZE_UI = 'customize_ui',
- ANOMALY_DETECTION = 'anomaly_detection',
- CSM = 'csm',
-}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
index 970365779a0a2..f27a3d56aab55 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
@@ -26,11 +26,14 @@ interface Props {
* aria-label for accessibility
*/
'aria-label'?: string;
+
+ maxWidth?: string;
}
export function ChartWrapper({
loading = false,
height = '100%',
+ maxWidth,
children,
...rest
}: Props) {
@@ -43,6 +46,7 @@ export function ChartWrapper({
height,
opacity,
transition: 'opacity 0.2s',
+ ...(maxWidth ? { maxWidth } : {}),
}}
{...(rest as HTMLAttributes)}
>
@@ -52,7 +56,12 @@ export function ChartWrapper({
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
index 9f9ffdf7168b8..213126ba4bf81 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
@@ -14,7 +14,7 @@ import {
PartitionLayout,
Settings,
} from '@elastic/charts';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import styled from 'styled-components';
import {
EUI_CHARTS_THEME_DARK,
EUI_CHARTS_THEME_LIGHT,
@@ -22,6 +22,10 @@ import {
import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public';
import { ChartWrapper } from '../ChartWrapper';
+const StyleChart = styled.div`
+ height: 100%;
+`;
+
interface Props {
options?: Array<{
count: number;
@@ -32,65 +36,47 @@ interface Props {
export function VisitorBreakdownChart({ options }: Props) {
const [darkMode] = useUiSetting$('theme:darkMode');
+ const euiChartTheme = darkMode
+ ? EUI_CHARTS_THEME_DARK
+ : EUI_CHARTS_THEME_LIGHT;
+
return (
-
-
-
- d.count as number}
- valueGetter="percent"
- percentFormatter={(d: number) =>
- `${Math.round((d + Number.EPSILON) * 100) / 100}%`
- }
- layers={[
- {
- groupByRollup: (d: Datum) => d.name,
- nodeLabel: (d: Datum) => d,
- // fillLabel: { textInvertible: true },
- shape: {
- fillColor: (d) => {
- const clrs = [
- euiLightVars.euiColorVis1_behindText,
- euiLightVars.euiColorVis0_behindText,
- euiLightVars.euiColorVis2_behindText,
- euiLightVars.euiColorVis3_behindText,
- euiLightVars.euiColorVis4_behindText,
- euiLightVars.euiColorVis5_behindText,
- euiLightVars.euiColorVis6_behindText,
- euiLightVars.euiColorVis7_behindText,
- euiLightVars.euiColorVis8_behindText,
- euiLightVars.euiColorVis9_behindText,
- ];
- return clrs[d.sortIndex];
+
+
+
+
+ d.count as number}
+ valueGetter="percent"
+ percentFormatter={(d: number) =>
+ `${Math.round((d + Number.EPSILON) * 100) / 100}%`
+ }
+ layers={[
+ {
+ groupByRollup: (d: Datum) => d.name,
+ shape: {
+ fillColor: (d) =>
+ euiChartTheme.theme.colors?.vizColors?.[d.sortIndex]!,
},
},
- },
- ]}
- config={{
- partitionLayout: PartitionLayout.sunburst,
- linkLabel: {
- maxCount: 32,
- fontSize: 14,
- },
- fontFamily: 'Arial',
- margin: { top: 0, bottom: 0, left: 0, right: 0 },
- minFontSize: 1,
- idealFontSizeJump: 1.1,
- outerSizeRatio: 0.9, // - 0.5 * Math.random(),
- emptySizeRatio: 0,
- circlePadding: 4,
- }}
- />
-
+ ]}
+ config={{
+ partitionLayout: PartitionLayout.sunburst,
+ linkLabel: { maximumSection: Infinity, maxCount: 0 },
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
+ outerSizeRatio: 1, // - 0.5 * Math.random(),
+ circlePadding: 4,
+ clockwiseSectors: false,
+ }}
+ />
+
+
);
}
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 67404ece3d2c7..f54a54211359c 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
@@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)`
export function ClientMetrics() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum/client-metrics',
params: {
@@ -36,7 +36,7 @@ export function ClientMetrics() {
}
return Promise.resolve(null);
},
- [start, end, serviceName, uiFilters]
+ [start, end, uiFilters]
);
const STAT_STYLE = { width: '240px' };
@@ -45,7 +45,7 @@ export function ClientMetrics() {
<>{numeral(data?.pageViews?.value).format('0 a') ?? '-'}>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx
new file mode 100644
index 0000000000000..fc2390acde0be
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import styled from 'styled-components';
+
+const ColoredSpan = styled.div`
+ height: 16px;
+ width: 100%;
+ cursor: pointer;
+`;
+
+const getSpanStyle = (
+ position: number,
+ inFocus: boolean,
+ hexCode: string,
+ percentage: number
+) => {
+ let first = position === 0 || percentage === 100;
+ let last = position === 2 || percentage === 100;
+ if (percentage === 100) {
+ first = true;
+ last = true;
+ }
+
+ const spanStyle: any = {
+ backgroundColor: hexCode,
+ opacity: !inFocus ? 1 : 0.3,
+ };
+ let borderRadius = '';
+
+ if (first) {
+ borderRadius = '4px 0 0 4px';
+ }
+ if (last) {
+ borderRadius = '0 4px 4px 0';
+ }
+ if (first && last) {
+ borderRadius = '4px';
+ }
+ spanStyle.borderRadius = borderRadius;
+
+ return spanStyle;
+};
+
+export function ColorPaletteFlexItem({
+ hexCode,
+ inFocus,
+ percentage,
+ tooltip,
+ position,
+}: {
+ hexCode: string;
+ position: number;
+ inFocus: boolean;
+ percentage: number;
+ tooltip: string;
+}) {
+ const spanStyle = getSpanStyle(position, inFocus, hexCode, percentage);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx
new file mode 100644
index 0000000000000..a4cbebf20b54c
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiFlexGroup,
+ euiPaletteForStatus,
+ EuiSpacer,
+ EuiStat,
+} from '@elastic/eui';
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { PaletteLegends } from './PaletteLegends';
+import { ColorPaletteFlexItem } from './ColorPaletteFlexItem';
+import {
+ AVERAGE_LABEL,
+ GOOD_LABEL,
+ LESS_LABEL,
+ MORE_LABEL,
+ POOR_LABEL,
+} from './translations';
+
+export interface Thresholds {
+ good: string;
+ bad: string;
+}
+
+interface Props {
+ title: string;
+ value: string;
+ ranks?: number[];
+ loading: boolean;
+ thresholds: Thresholds;
+}
+
+export function getCoreVitalTooltipMessage(
+ thresholds: Thresholds,
+ position: number,
+ title: string,
+ percentage: number
+) {
+ const good = position === 0;
+ const bad = position === 2;
+ const average = !good && !bad;
+
+ return i18n.translate('xpack.apm.csm.dashboard.webVitals.palette.tooltip', {
+ defaultMessage:
+ '{percentage} % of users have {exp} experience because the {title} takes {moreOrLess} than {value}{averageMessage}.',
+ values: {
+ percentage,
+ title: title?.toLowerCase(),
+ exp: good ? GOOD_LABEL : bad ? POOR_LABEL : AVERAGE_LABEL,
+ moreOrLess: bad || average ? MORE_LABEL : LESS_LABEL,
+ value: good || average ? thresholds.good : thresholds.bad,
+ averageMessage: average
+ ? i18n.translate('xpack.apm.rum.coreVitals.averageMessage', {
+ defaultMessage: ' and less than {bad}',
+ values: { bad: thresholds.bad },
+ })
+ : '',
+ },
+ });
+}
+
+export function CoreVitalItem({
+ loading,
+ title,
+ value,
+ thresholds,
+ ranks = [100, 0, 0],
+}: Props) {
+ const palette = euiPaletteForStatus(3);
+
+ const [inFocusInd, setInFocusInd] = useState(null);
+
+ const biggestValIndex = ranks.indexOf(Math.max(...ranks));
+
+ return (
+ <>
+
+
+
+ {palette.map((hexCode, ind) => (
+
+ ))}
+
+
+ {
+ setInFocusInd(ind);
+ }}
+ />
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx
new file mode 100644
index 0000000000000..84cc5f1ddb230
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHealth,
+ euiPaletteForStatus,
+ EuiToolTip,
+} from '@elastic/eui';
+import styled from 'styled-components';
+import { getCoreVitalTooltipMessage, Thresholds } from './CoreVitalItem';
+
+const PaletteLegend = styled(EuiHealth)`
+ &:hover {
+ cursor: pointer;
+ text-decoration: underline;
+ background-color: #e7f0f7;
+ }
+`;
+
+interface Props {
+ onItemHover: (ind: number | null) => void;
+ ranks: number[];
+ thresholds: Thresholds;
+ title: string;
+}
+
+export function PaletteLegends({
+ ranks,
+ title,
+ onItemHover,
+ thresholds,
+}: Props) {
+ const palette = euiPaletteForStatus(3);
+
+ return (
+
+ {palette.map((color, ind) => (
+ {
+ onItemHover(ind);
+ }}
+ onMouseLeave={() => {
+ onItemHover(null);
+ }}
+ >
+
+ {ranks?.[ind]}%
+
+
+ ))}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx
new file mode 100644
index 0000000000000..a611df00f1e65
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { EuiThemeProvider } from '../../../../../../../observability/public';
+import { CoreVitalItem } from '../CoreVitalItem';
+import { LCP_LABEL } from '../translations';
+
+storiesOf('app/RumDashboard/WebCoreVitals', module)
+ .addDecorator((storyFn) => {storyFn()})
+ .add(
+ 'Basic',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '50% Good',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '100% Bad',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '100% Average',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ );
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
new file mode 100644
index 0000000000000..e8305a6aef0d4
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { useFetcher } from '../../../../hooks/useFetcher';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { CLS_LABEL, FID_LABEL, LCP_LABEL } from './translations';
+import { CoreVitalItem } from './CoreVitalItem';
+
+const CoreVitalsThresholds = {
+ LCP: { good: '2.5s', bad: '4.0s' },
+ FID: { good: '100ms', bad: '300ms' },
+ CLS: { good: '0.1', bad: '0.25' },
+};
+
+export function CoreVitals() {
+ const { urlParams, uiFilters } = useUrlParams();
+
+ const { start, end, serviceName } = urlParams;
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (start && end && serviceName) {
+ return callApmApi({
+ pathname: '/api/apm/rum-client/web-core-vitals',
+ params: {
+ query: { start, end, uiFilters: JSON.stringify(uiFilters) },
+ },
+ });
+ }
+ return Promise.resolve(null);
+ },
+ [start, end, serviceName, uiFilters]
+ );
+
+ const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {};
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts
new file mode 100644
index 0000000000000..136dfb279e336
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const LCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.lcp', {
+ defaultMessage: 'Largest contentful paint',
+});
+
+export const FID_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fip', {
+ defaultMessage: 'First input delay',
+});
+
+export const CLS_LABEL = i18n.translate('xpack.apm.rum.coreVitals.cls', {
+ defaultMessage: 'Cumulative layout shift',
+});
+
+export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', {
+ defaultMessage: 'First contentful paint',
+});
+
+export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', {
+ defaultMessage: 'Total blocking time',
+});
+
+export const POOR_LABEL = i18n.translate('xpack.apm.rum.coreVitals.poor', {
+ defaultMessage: 'a poor',
+});
+
+export const GOOD_LABEL = i18n.translate('xpack.apm.rum.coreVitals.good', {
+ defaultMessage: 'a good',
+});
+
+export const AVERAGE_LABEL = i18n.translate(
+ 'xpack.apm.rum.coreVitals.average',
+ {
+ defaultMessage: 'an average',
+ }
+);
+
+export const MORE_LABEL = i18n.translate('xpack.apm.rum.coreVitals.more', {
+ defaultMessage: 'more',
+});
+
+export const LESS_LABEL = i18n.translate('xpack.apm.rum.coreVitals.less', {
+ defaultMessage: 'less',
+});
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx
new file mode 100644
index 0000000000000..deaeed70e572b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiButtonEmpty,
+ EuiHideFor,
+ EuiShowFor,
+ EuiButtonIcon,
+} from '@elastic/eui';
+import { I18LABELS } from '../translations';
+import { PercentileRange } from './index';
+
+interface Props {
+ percentileRange: PercentileRange;
+ setPercentileRange: (value: PercentileRange) => void;
+}
+export function ResetPercentileZoom({
+ percentileRange,
+ setPercentileRange,
+}: Props) {
+ const isDisabled =
+ percentileRange.min === null && percentileRange.max === null;
+ const onClick = () => {
+ setPercentileRange({ min: null, max: null });
+ };
+ return (
+ <>
+
+
+
+
+
+ {I18LABELS.resetZoom}
+
+
+ >
+ );
+}
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 3e35f15254937..f63b914c73398 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
@@ -5,19 +5,14 @@
*/
import React, { useState } from 'react';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';
import { I18LABELS } from '../translations';
import { BreakdownFilter } from '../Breakdowns/BreakdownFilter';
import { PageLoadDistChart } from '../Charts/PageLoadDistChart';
import { BreakdownItem } from '../../../../../typings/ui_filters';
+import { ResetPercentileZoom } from './ResetPercentileZoom';
export interface PercentileRange {
min?: number | null;
@@ -27,7 +22,7 @@ export interface PercentileRange {
export function PageLoadDistribution() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const [percentileRange, setPercentileRange] = useState({
min: null,
@@ -38,7 +33,7 @@ export function PageLoadDistribution() {
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/page-load-distribution',
params: {
@@ -58,14 +53,7 @@ export function PageLoadDistribution() {
}
return Promise.resolve(null);
},
- [
- end,
- start,
- serviceName,
- uiFilters,
- percentileRange.min,
- percentileRange.max,
- ]
+ [end, start, uiFilters, percentileRange.min, percentileRange.max]
);
const onPercentileChange = (min: number, max: number) => {
@@ -81,18 +69,10 @@ export function PageLoadDistribution() {
- {
- setPercentileRange({ min: null, max: null });
- }}
- disabled={
- percentileRange.min === null && percentileRange.max === null
- }
- >
- {I18LABELS.resetZoom}
-
+
{
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { min: minP, max: maxP } = percentileRange ?? {};
return useFetcher(
(callApmApi) => {
- if (start && end && serviceName && field && value) {
+ if (start && end && field && value) {
return callApmApi({
pathname: '/api/apm/rum-client/page-load-distribution/breakdown',
params: {
@@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => {
});
}
},
- [end, start, serviceName, uiFilters, field, value, minP, maxP]
+ [end, start, uiFilters, field, value, minP, maxP]
);
};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
index a67f6dd8e3cb5..62ecc4ddbaaca 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters';
export function PageViewsTrend() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const [breakdown, setBreakdown] = useState(null);
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/page-view-trends',
params: {
@@ -41,7 +41,7 @@ export function PageViewsTrend() {
}
return Promise.resolve(undefined);
},
- [end, start, serviceName, uiFilters, breakdown]
+ [end, start, uiFilters, breakdown]
);
return (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
index 24d4470736de0..f05c07e8512ac 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
@@ -17,6 +17,7 @@ import { PageViewsTrend } from './PageViewsTrend';
import { PageLoadDistribution } from './PageLoadDistribution';
import { I18LABELS } from './translations';
import { VisitorBreakdown } from './VisitorBreakdown';
+import { CoreVitals } from './CoreVitals';
export function RumDashboard() {
return (
@@ -26,7 +27,7 @@ export function RumDashboard() {
- {I18LABELS.pageLoadTimes}
+ {I18LABELS.pageLoadDuration}
@@ -37,13 +38,29 @@ export function RumDashboard() {
-
-
-
-
+
+
+ {I18LABELS.coreWebVitals}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
index 5c68ebb1667ab..e18875f32ff72 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
@@ -5,20 +5,20 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart';
-import { VisitorBreakdownLabel } from '../translations';
+import { I18LABELS, VisitorBreakdownLabel } from '../translations';
import { useFetcher } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
export function VisitorBreakdown() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/visitor-breakdown',
params: {
@@ -32,32 +32,29 @@ export function VisitorBreakdown() {
}
return Promise.resolve(null);
},
- [end, start, serviceName, uiFilters]
+ [end, start, uiFilters]
);
return (
<>
-
+
{VisitorBreakdownLabel}
+
-
-
- Browser
-
-
-
-
-
- Operating System
+
+ {I18LABELS.browser}
+
+
-
-
- Device
+
+ {I18LABELS.operatingSystem}
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
index 66eeaf433d2a1..660ed5a92a0e6 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
@@ -25,6 +25,12 @@ export const I18LABELS = {
pageLoadTimes: i18n.translate('xpack.apm.rum.dashboard.pageLoadTimes.label', {
defaultMessage: 'Page load times',
}),
+ pageLoadDuration: i18n.translate(
+ 'xpack.apm.rum.dashboard.pageLoadDuration.label',
+ {
+ defaultMessage: 'Page load duration',
+ }
+ ),
pageLoadDistribution: i18n.translate(
'xpack.apm.rum.dashboard.pageLoadDistribution.label',
{
@@ -46,6 +52,18 @@ export const I18LABELS = {
seconds: i18n.translate('xpack.apm.rum.filterGroup.seconds', {
defaultMessage: 'seconds',
}),
+ coreWebVitals: i18n.translate('xpack.apm.rum.filterGroup.coreWebVitals', {
+ defaultMessage: 'Core web vitals',
+ }),
+ browser: i18n.translate('xpack.apm.rum.visitorBreakdown.browser', {
+ defaultMessage: 'Browser',
+ }),
+ operatingSystem: i18n.translate(
+ 'xpack.apm.rum.visitorBreakdown.operatingSystem',
+ {
+ defaultMessage: 'Operating system',
+ }
+ ),
};
export const VisitorBreakdownLabel = i18n.translate(
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
index 2f35e329720de..cbb6d9a8fbe41 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
@@ -10,7 +10,6 @@ import React from 'react';
import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name';
import { useAgentName } from '../../../hooks/useAgentName';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import { EuiTabLink } from '../../shared/EuiTabLink';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { MetricOverviewLink } from '../../shared/Links/apm/MetricOverviewLink';
@@ -24,20 +23,14 @@ import { ServiceNodeOverview } from '../ServiceNodeOverview';
import { TransactionOverview } from '../TransactionOverview';
interface Props {
+ serviceName: string;
tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map';
}
-export function ServiceDetailTabs({ tab }: Props) {
- const { urlParams } = useUrlParams();
- const { serviceName } = urlParams;
+export function ServiceDetailTabs({ serviceName, tab }: Props) {
const { agentName } = useAgentName();
const { serviceMapEnabled } = useApmPluginContext().config;
- if (!serviceName) {
- // this never happens, urlParams type is not accurate enough
- throw new Error('Service name was not defined');
- }
-
const transactionsTab = {
link: (
@@ -46,7 +39,7 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => ,
name: 'transactions',
};
@@ -59,7 +52,7 @@ export function ServiceDetailTabs({ tab }: Props) {
),
render: () => {
- return ;
+ return ;
},
name: 'errors',
};
@@ -75,7 +68,7 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => ,
name: 'nodes',
};
tabs.push(nodesListTab);
@@ -88,7 +81,9 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => (
+
+ ),
name: 'metrics',
};
tabs.push(metricsTab);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
index b5a4ca4799afd..67c4a7c4cde1b 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
@@ -5,27 +5,26 @@
*/
import {
+ EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
- EuiButtonEmpty,
} from '@elastic/eui';
-import React from 'react';
import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { ApmHeader } from '../../shared/ApmHeader';
-import { ServiceDetailTabs } from './ServiceDetailTabs';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import { AlertIntegrations } from './AlertIntegrations';
-import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
+import { ServiceDetailTabs } from './ServiceDetailTabs';
-interface Props {
+interface Props extends RouteComponentProps<{ serviceName: string }> {
tab: React.ComponentProps['tab'];
}
-export function ServiceDetails({ tab }: Props) {
+export function ServiceDetails({ match, tab }: Props) {
const plugin = useApmPluginContext();
- const { urlParams } = useUrlParams();
- const { serviceName } = urlParams;
+ const { serviceName } = match.params;
const capabilities = plugin.core.application.capabilities;
const canReadAlerts = !!capabilities.apm['alerting:show'];
const canSaveAlerts = !!capabilities.apm['alerting:save'];
@@ -76,7 +75,7 @@ export function ServiceDetails({ tab }: Props) {
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
index 9b01f9ebb7e99..2fb500f3c9916 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
@@ -21,11 +21,14 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
interface ServiceMetricsProps {
agentName: string;
+ serviceName: string;
}
-export function ServiceMetrics({ agentName }: ServiceMetricsProps) {
+export function ServiceMetrics({
+ agentName,
+ serviceName,
+}: ServiceMetricsProps) {
const { urlParams } = useUrlParams();
- const { serviceName, serviceNodeName } = urlParams;
const { data } = useServiceMetricCharts(urlParams, agentName);
const { start, end } = urlParams;
@@ -34,12 +37,11 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) {
filterNames: ['host', 'containerId', 'podName', 'serviceVersion'],
params: {
serviceName,
- serviceNodeName,
},
projection: Projection.metrics,
showCount: false,
}),
- [serviceName, serviceNodeName]
+ [serviceName]
);
return (
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
index eced7457318d8..c6f7e68e4f4d0 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
@@ -8,14 +8,20 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ServiceNodeMetrics } from '.';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
+import { RouteComponentProps } from 'react-router-dom';
describe('ServiceNodeMetrics', () => {
describe('render', () => {
it('renders', () => {
+ const props = ({} as unknown) as RouteComponentProps<{
+ serviceName: string;
+ serviceNodeName: string;
+ }>;
+
expect(() =>
shallow(
-
+
)
).not.toThrowError();
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
index e81968fb298fa..84a1920d17fa8 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
@@ -5,30 +5,31 @@
*/
import {
+ EuiCallOut,
+ EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
- EuiTitle,
EuiHorizontalRule,
- EuiFlexGrid,
EuiPanel,
EuiSpacer,
EuiStat,
+ EuiTitle,
EuiToolTip,
- EuiCallOut,
} from '@elastic/eui';
-import React from 'react';
import { i18n } from '@kbn/i18n';
-import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import styled from 'styled-components';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
-import { ApmHeader } from '../../shared/ApmHeader';
-import { useUrlParams } from '../../../hooks/useUrlParams';
+import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { useAgentName } from '../../../hooks/useAgentName';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
-import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { px, truncate, unit } from '../../../style/variables';
+import { ApmHeader } from '../../shared/ApmHeader';
import { MetricsChart } from '../../shared/charts/MetricsChart';
-import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher';
-import { truncate, px, unit } from '../../../style/variables';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
const INITIAL_DATA = {
@@ -41,17 +42,21 @@ const Truncate = styled.span`
${truncate(px(unit * 12))}
`;
-export function ServiceNodeMetrics() {
- const { urlParams, uiFilters } = useUrlParams();
- const { serviceName, serviceNodeName } = urlParams;
+type ServiceNodeMetricsProps = RouteComponentProps<{
+ serviceName: string;
+ serviceNodeName: string;
+}>;
+export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) {
+ const { urlParams, uiFilters } = useUrlParams();
+ const { serviceName, serviceNodeName } = match.params;
const { agentName } = useAgentName();
const { data } = useServiceMetricCharts(urlParams, agentName);
const { start, end } = urlParams;
const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher(
(callApmApi) => {
- if (serviceName && serviceNodeName && start && end) {
+ if (start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
@@ -167,7 +172,7 @@ export function ServiceNodeMetrics() {
)}
- {agentName && serviceNodeName && (
+ {agentName && (
{data.charts.map((chart) => (
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
index 9940a7aabb219..28477d2448899 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
@@ -33,9 +33,13 @@ const ServiceNodeName = styled.div`
${truncate(px(8 * unit))}
`;
-function ServiceNodeOverview() {
+interface ServiceNodeOverviewProps {
+ serviceName: string;
+}
+
+function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
const { uiFilters, urlParams } = useUrlParams();
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const localFiltersConfig: React.ComponentProps = useMemo(
() => ({
@@ -50,7 +54,7 @@ function ServiceNodeOverview() {
const { data: items = [] } = useFetcher(
(callApmApi) => {
- if (!serviceName || !start || !end) {
+ if (!start || !end) {
return undefined;
}
return callApmApi({
@@ -70,10 +74,6 @@ function ServiceNodeOverview() {
[serviceName, start, end, uiFilters]
);
- if (!serviceName) {
- return null;
- }
-
const columns: Array> = [
{
name: (
diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
index bbaf6340e18f7..8d37a8e54d87c 100644
--- a/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
@@ -5,63 +5,84 @@
*/
import { render } from '@testing-library/react';
import { shallow } from 'enzyme';
-import React from 'react';
+import React, { ReactNode } from 'react';
+import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
import { TraceLink } from '../';
+import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
+import {
+ mockApmPluginContextValue,
+ MockApmPluginContextWrapper,
+} from '../../../../context/ApmPluginContext/MockApmPluginContext';
import * as hooks from '../../../../hooks/useFetcher';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
-import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
-const renderOptions = { wrapper: MockApmPluginContextWrapper };
+function Wrapper({ children }: { children?: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
-jest.mock('../../Main/route_config', () => ({
- routes: [
- {
- path: '/services/:serviceName/transactions/view',
- name: 'transaction_name',
- },
- {
- path: '/traces',
- name: 'traces',
- },
- ],
-}));
+const renderOptions = { wrapper: Wrapper };
describe('TraceLink', () => {
afterAll(() => {
jest.clearAllMocks();
});
- it('renders transition page', () => {
- const component = render(, renderOptions);
+
+ it('renders a transition page', () => {
+ const props = ({
+ match: { params: { traceId: 'x' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = render(, renderOptions);
+
expect(component.getByText('Fetching trace...')).toBeDefined();
});
- it('renders trace page when transaction is not found', () => {
- jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
- urlParams: {
- traceIdLink: '123',
- rangeFrom: 'now-24h',
- rangeTo: 'now',
- },
- refreshTimeRange: jest.fn(),
- uiFilters: {},
- });
- jest.spyOn(hooks, 'useFetcher').mockReturnValue({
- data: { transaction: undefined },
- status: hooks.FETCH_STATUS.SUCCESS,
- refetch: jest.fn(),
- });
+ describe('when no transaction is found', () => {
+ it('renders a trace page', () => {
+ jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
+ urlParams: {
+ rangeFrom: 'now-24h',
+ rangeTo: 'now',
+ },
+ refreshTimeRange: jest.fn(),
+ uiFilters: {},
+ });
+ jest.spyOn(hooks, 'useFetcher').mockReturnValue({
+ data: { transaction: undefined },
+ status: hooks.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ });
+
+ const props = ({
+ match: { params: { traceId: '123' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = shallow();
- const component = shallow();
- expect(component.prop('to')).toEqual(
- '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
- );
+ expect(component.prop('to')).toEqual(
+ '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
+ );
+ });
});
describe('transaction page', () => {
beforeAll(() => {
jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
urlParams: {
- traceIdLink: '123',
rangeFrom: 'now-24h',
rangeTo: 'now',
},
@@ -69,6 +90,7 @@ describe('TraceLink', () => {
uiFilters: {},
});
});
+
it('renders with date range params', () => {
const transaction = {
service: { name: 'foo' },
@@ -84,7 +106,12 @@ describe('TraceLink', () => {
status: hooks.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
- const component = shallow();
+
+ const props = ({
+ match: { params: { traceId: '123' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = shallow();
+
expect(component.prop('to')).toEqual(
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now'
);
diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
index 55ab275002b4e..584af956c2022 100644
--- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
@@ -6,7 +6,7 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
-import { Redirect } from 'react-router-dom';
+import { Redirect, RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import url from 'url';
import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames';
@@ -58,9 +58,10 @@ const redirectToTracePage = ({
},
});
-export function TraceLink() {
+export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) {
+ const { traceId } = match.params;
const { urlParams } = useUrlParams();
- const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams;
+ const { rangeFrom, rangeTo } = urlParams;
const { data = { transaction: null }, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index 515fcbc88c901..bab31c9a460d0 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -13,6 +13,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import React, { useMemo } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
@@ -29,7 +30,10 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { TransactionDistribution } from './Distribution';
import { WaterfallWithSummmary } from './WaterfallWithSummmary';
-export function TransactionDetails() {
+type TransactionDetailsProps = RouteComponentProps<{ serviceName: string }>;
+
+export function TransactionDetails({ match }: TransactionDetailsProps) {
+ const { serviceName } = match.params;
const location = useLocation();
const { urlParams } = useUrlParams();
const {
@@ -41,7 +45,7 @@ export function TransactionDetails() {
const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall(
urlParams
);
- const { transactionName, transactionType, serviceName } = urlParams;
+ const { transactionName, transactionType } = urlParams;
useTrackPageview({ app: 'apm', path: 'transaction_details' });
useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 });
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
index 81fe9e2282667..b7d1b93600a73 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
@@ -12,7 +12,6 @@ import {
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
-import { omit } from 'lodash';
import React from 'react';
import { Router } from 'react-router-dom';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
@@ -42,7 +41,7 @@ function setup({
}) {
const defaultLocation = {
pathname: '/services/foo/transactions',
- search: fromQuery(omit(urlParams, 'serviceName')),
+ search: fromQuery(urlParams),
} as any;
history.replace({
@@ -60,7 +59,7 @@ function setup({
-
+
@@ -87,9 +86,7 @@ describe('TransactionOverview', () => {
it('should redirect to first type', () => {
setup({
serviceTransactionTypes: ['firstType', 'secondType'],
- urlParams: {
- serviceName: 'MyServiceName',
- },
+ urlParams: {},
});
expect(history.replace).toHaveBeenCalledWith(
expect.objectContaining({
@@ -107,7 +104,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
- serviceName: 'MyServiceName',
},
});
@@ -122,7 +118,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
- serviceName: 'MyServiceName',
},
});
@@ -143,7 +138,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType'],
urlParams: {
transactionType: 'firstType',
- serviceName: 'MyServiceName',
},
});
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 5999988abe848..544e2450fe5d9 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -59,11 +59,14 @@ function getRedirectLocation({
}
}
-export function TransactionOverview() {
+interface TransactionOverviewProps {
+ serviceName: string;
+}
+
+export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
const location = useLocation();
const { urlParams } = useUrlParams();
-
- const { serviceName, transactionType } = urlParams;
+ const { transactionType } = urlParams;
// TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context?
const serviceTransactionTypes = useServiceTransactionTypes(urlParams);
diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
index 9a61e773d73bf..7e5c789507e07 100644
--- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
@@ -8,7 +8,7 @@ import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
@@ -63,10 +63,11 @@ function getOptions(environments: string[]) {
export function EnvironmentFilter() {
const history = useHistory();
const location = useLocation();
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { uiFilters, urlParams } = useUrlParams();
const { environment } = uiFilters;
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environments, status = 'loading' } = useEnvironments({
serviceName,
start,
diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
index 7344839795955..7b284696477f3 100644
--- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
@@ -3,21 +3,21 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { EuiFieldNumber } from '@elastic/eui';
+import { EuiFieldNumber, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isFinite } from 'lodash';
-import { EuiSelect } from '@elastic/eui';
+import React from 'react';
+import { useParams } from 'react-router-dom';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
-import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
-import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
-import { useEnvironments } from '../../../hooks/useEnvironments';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import {
ENVIRONMENT_ALL,
getEnvironmentLabel,
} from '../../../../common/environment_filter_values';
+import { useEnvironments } from '../../../hooks/useEnvironments';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
+import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
export interface ErrorRateAlertTriggerParams {
windowSize: number;
@@ -34,9 +34,9 @@ interface Props {
export function ErrorRateAlertTrigger(props: Props) {
const { setAlertParams, setAlertProperty, alertParams } = props;
-
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
const defaults = {
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
index 5bac01cfaf55d..74d7ace20dae0 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
@@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESFilter } from '../../../../typings/elasticsearch';
import {
- TRANSACTION_TYPE,
ERROR_GROUP_ID,
PROCESSOR_EVENT,
- TRANSACTION_NAME,
SERVICE_NAME,
+ TRANSACTION_NAME,
+ TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
+import { UIProcessorEvent } from '../../../../common/processor_event';
+import { ESFilter } from '../../../../typings/elasticsearch';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
-export function getBoolFilter(urlParams: IUrlParams) {
- const { start, end, serviceName, processorEvent } = urlParams;
+export function getBoolFilter({
+ groupId,
+ processorEvent,
+ serviceName,
+ urlParams,
+}: {
+ groupId?: string;
+ processorEvent?: UIProcessorEvent;
+ serviceName?: string;
+ urlParams: IUrlParams;
+}) {
+ const { start, end } = urlParams;
if (!start || !end) {
throw new Error('Date range was not defined');
@@ -63,9 +74,9 @@ export function getBoolFilter(urlParams: IUrlParams) {
term: { [PROCESSOR_EVENT]: 'error' },
});
- if (urlParams.errorGroupId) {
+ if (groupId) {
boolFilter.push({
- term: { [ERROR_GROUP_ID]: urlParams.errorGroupId },
+ term: { [ERROR_GROUP_ID]: groupId },
});
}
break;
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
index a52676ee89590..efd1446f21b21 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { startsWith, uniqueId } from 'lodash';
import React, { useState } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import styled from 'styled-components';
import {
esKuery,
@@ -22,6 +22,7 @@ import { fromQuery, toQuery } from '../Links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
// @ts-expect-error
import { Typeahead } from './Typeahead';
+import { useProcessorEvent } from './use_processor_event';
const Container = styled.div`
margin-bottom: 10px;
@@ -38,6 +39,10 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
}
export function KueryBar() {
+ const { groupId, serviceName } = useParams<{
+ groupId?: string;
+ serviceName?: string;
+ }>();
const history = useHistory();
const [state, setState] = useState({
suggestions: [],
@@ -49,7 +54,7 @@ export function KueryBar() {
let currentRequestCheck;
- const { processorEvent } = urlParams;
+ const processorEvent = useProcessorEvent();
const examples = {
transaction: 'transaction.duration.us > 300000',
@@ -98,7 +103,12 @@ export function KueryBar() {
(await data.autocomplete.getQuerySuggestions({
language: 'kuery',
indexPatterns: [indexPattern],
- boolFilter: getBoolFilter(urlParams),
+ boolFilter: getBoolFilter({
+ groupId,
+ processorEvent,
+ serviceName,
+ urlParams,
+ }),
query: inputValue,
selectionStart,
selectionEnd: selectionStart,
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts
new file mode 100644
index 0000000000000..1e8686f0fe5ee
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useLocation } from 'react-router-dom';
+import {
+ ProcessorEvent,
+ UIProcessorEvent,
+} from '../../../../common/processor_event';
+
+/**
+ * Infer the processor.event to used based on the route path
+ */
+export function useProcessorEvent(): UIProcessorEvent | undefined {
+ const { pathname } = useLocation();
+ const paths = pathname.split('/').slice(1);
+ const pageName = paths[0];
+
+ switch (pageName) {
+ case 'services':
+ let servicePageName = paths[2];
+
+ if (servicePageName === 'nodes' && paths.length > 3) {
+ servicePageName = 'metrics';
+ }
+
+ switch (servicePageName) {
+ case 'transactions':
+ return ProcessorEvent.transaction;
+ case 'errors':
+ return ProcessorEvent.error;
+ case 'metrics':
+ return ProcessorEvent.metric;
+ case 'nodes':
+ return ProcessorEvent.metric;
+
+ default:
+ return undefined;
+ }
+ case 'traces':
+ return ProcessorEvent.transaction;
+ default:
+ return undefined;
+ }
+}
diff --git a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
index 6d90a10891c21..86dc7f5a90475 100644
--- a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
@@ -6,7 +6,7 @@
import React, { useEffect } from 'react';
import { EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
-import { useUrlParams } from '../../../hooks/useUrlParams';
+import { useParams } from 'react-router-dom';
interface Props {
alertTypeName: string;
@@ -17,7 +17,7 @@ interface Props {
}
export function ServiceAlertTrigger(props: Props) {
- const { urlParams } = useUrlParams();
+ const { serviceName } = useParams<{ serviceName?: string }>();
const {
fields,
@@ -29,7 +29,7 @@ export function ServiceAlertTrigger(props: Props) {
const params: Record = {
...defaults,
- serviceName: urlParams.serviceName!,
+ serviceName,
};
useEffect(() => {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
index ba12b11c9527d..3c1669c39ac4c 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
@@ -40,12 +40,12 @@ interface Props {
export function TransactionDurationAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
-
+ const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
if (!transactionTypes.length) {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
index 911c51013a844..20e0a3f27c4a4 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
@@ -42,9 +42,10 @@ interface Props {
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
+ const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
const supportedTransactionTypes = transactionTypes.filter((transactionType) =>
[TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType)
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
index f829b5841efa9..52b0470d31552 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
@@ -4,13 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIconTip } from '@elastic/eui';
+import { EuiFlexItem, EuiIconTip, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React from 'react';
-import { EuiFlexItem } from '@elastic/eui';
+import { useParams } from 'react-router-dom';
import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import { EuiText } from '@elastic/eui';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
@@ -32,16 +31,14 @@ const ShiftedEuiText = styled(EuiText)`
`;
export function MLHeader({ hasValidMlLicense, mlJobId }: Props) {
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
if (!hasValidMlLicense || !mlJobId) {
return null;
}
- const { serviceName, kuery, transactionType } = urlParams;
- if (!serviceName) {
- return null;
- }
+ const { kuery, transactionType } = urlParams;
const hasKuery = !isEmpty(kuery);
const icon = hasKuery ? (
diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
index 8334efffbd511..48206572932b1 100644
--- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
+++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
@@ -39,6 +39,7 @@ const mockCore = {
apm: {},
},
currentAppId$: new Observable(),
+ navigateToUrl: (url: string) => {},
},
chrome: {
docTitle: { change: () => {} },
diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
index 801c1d7e53f2e..7df35bc443226 100644
--- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
+++ b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
@@ -5,7 +5,7 @@
*/
import React, { ReactNode, useMemo, useState } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { useFetcher } from '../hooks/useFetcher';
import { useUrlParams } from '../hooks/useUrlParams';
@@ -20,9 +20,10 @@ const ChartsSyncContext = React.createContext<{
function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
const history = useHistory();
const [time, setTime] = useState(null);
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { environment } = uiFilters;
const { data = { annotations: [] } } = useFetcher(
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
index fbb79eae6a136..9989e568953f5 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
@@ -41,24 +41,6 @@ describe('UrlParamsContext', () => {
moment.tz.setDefault('');
});
- it('should have default params', () => {
- const location = {
- pathname: '/services/opbeans-node/transactions',
- } as Location;
-
- jest
- .spyOn(Date, 'now')
- .mockImplementation(() => new Date('2000-06-15T12:00:00Z').getTime());
- const wrapper = mountParams(location);
- const params = getDataFromOutput(wrapper);
-
- expect(params).toEqual({
- serviceName: 'opbeans-node',
- page: 0,
- processorEvent: 'transaction',
- });
- });
-
it('should read values in from location', () => {
const location = {
pathname: '/test/pathname',
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
index 65514ff71d02b..45db4dcc94cce 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
@@ -7,18 +7,6 @@
import { compact, pickBy } from 'lodash';
import datemath from '@elastic/datemath';
import { IUrlParams } from './types';
-import {
- ProcessorEvent,
- UIProcessorEvent,
-} from '../../../common/processor_event';
-
-interface PathParams {
- processorEvent?: UIProcessorEvent;
- serviceName?: string;
- errorGroupId?: string;
- serviceNodeName?: string;
- traceId?: string;
-}
export function getParsedDate(rawDate?: string, opts = {}) {
if (rawDate) {
@@ -67,68 +55,3 @@ export function getPathAsArray(pathname: string = '') {
export function removeUndefinedProps(obj: T): Partial {
return pickBy(obj, (value) => value !== undefined);
}
-
-export function getPathParams(pathname: string = ''): PathParams {
- const paths = getPathAsArray(pathname);
- const pageName = paths[0];
- // TODO: use react router's real match params instead of guessing the path order
-
- switch (pageName) {
- case 'services':
- let servicePageName = paths[2];
- const serviceName = paths[1];
- const serviceNodeName = paths[3];
-
- if (servicePageName === 'nodes' && paths.length > 3) {
- servicePageName = 'metrics';
- }
-
- switch (servicePageName) {
- case 'transactions':
- return {
- processorEvent: ProcessorEvent.transaction,
- serviceName,
- };
- case 'errors':
- return {
- processorEvent: ProcessorEvent.error,
- serviceName,
- errorGroupId: paths[3],
- };
- case 'metrics':
- return {
- processorEvent: ProcessorEvent.metric,
- serviceName,
- serviceNodeName,
- };
- case 'nodes':
- return {
- processorEvent: ProcessorEvent.metric,
- serviceName,
- };
- case 'service-map':
- return {
- serviceName,
- };
- default:
- return {};
- }
-
- case 'traces':
- return {
- processorEvent: ProcessorEvent.transaction,
- };
- case 'link-to':
- const link = paths[1];
- switch (link) {
- case 'trace':
- return {
- traceId: paths[2],
- };
- default:
- return {};
- }
- default:
- return {};
- }
-}
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
index 2201e162904a2..8feb4ac1858d1 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
@@ -7,7 +7,6 @@
import { Location } from 'history';
import { IUrlParams } from './types';
import {
- getPathParams,
removeUndefinedProps,
getStart,
getEnd,
@@ -26,14 +25,6 @@ type TimeUrlParams = Pick<
>;
export function resolveUrlParams(location: Location, state: TimeUrlParams) {
- const {
- processorEvent,
- serviceName,
- serviceNodeName,
- errorGroupId,
- traceId: traceIdLink,
- } = getPathParams(location.pathname);
-
const query = toQuery(location.search);
const {
@@ -85,15 +76,6 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
transactionType,
searchTerm: toString(searchTerm),
- // path params
- processorEvent,
- serviceName,
- traceIdLink,
- errorGroupId,
- serviceNodeName: serviceNodeName
- ? decodeURIComponent(serviceNodeName)
- : serviceNodeName,
-
// ui filters
environment,
...localUIFilters,
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
index 7b50a705afa33..574eca3b74f70 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
@@ -6,12 +6,10 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config';
-import { UIProcessorEvent } from '../../../common/processor_event';
export type IUrlParams = {
detailTab?: string;
end?: string;
- errorGroupId?: string;
flyoutDetailTab?: string;
kuery?: string;
environment?: string;
@@ -19,7 +17,6 @@ export type IUrlParams = {
rangeTo?: string;
refreshInterval?: number;
refreshPaused?: boolean;
- serviceName?: string;
sortDirection?: string;
sortField?: string;
start?: string;
@@ -30,8 +27,5 @@ export type IUrlParams = {
waterfallItemId?: string;
page?: number;
pageSize?: number;
- serviceNodeName?: string;
searchTerm?: string;
- processorEvent?: UIProcessorEvent;
- traceIdLink?: string;
} & Partial>;
diff --git a/x-pack/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/useAgentName.ts
index 7a11b662f06f0..1f8a3b916ecd0 100644
--- a/x-pack/plugins/apm/public/hooks/useAgentName.ts
+++ b/x-pack/plugins/apm/public/hooks/useAgentName.ts
@@ -3,13 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { useParams } from 'react-router-dom';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
export function useAgentName() {
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data: agentName, error, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
index 78f022ec6b8b5..f4a981ff0975b 100644
--- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
+++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useParams } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent';
-import { IUrlParams } from '../context/UrlParamsContext/types';
import { useUiFilters } from '../context/UrlParamsContext';
+import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
const INITIAL_DATA: MetricsChartsByAgentAPIResponse = {
@@ -18,7 +19,8 @@ export function useServiceMetricCharts(
urlParams: IUrlParams,
agentName?: string
) {
- const { serviceName, start, end, serviceNodeName } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = INITIAL_DATA, error, status } = useFetcher(
(callApmApi) => {
@@ -31,14 +33,13 @@ export function useServiceMetricCharts(
start,
end,
agentName,
- serviceNodeName,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
},
- [serviceName, start, end, agentName, serviceNodeName, uiFilters]
+ [serviceName, start, end, agentName, uiFilters]
);
return {
diff --git a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
index 227cd849d6c7c..4e110ac2d4380 100644
--- a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
+++ b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useParams } from 'react-router-dom';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
const INITIAL_DATA = { transactionTypes: [] };
export function useServiceTransactionTypes(urlParams: IUrlParams) {
- const { serviceName, start, end } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end } = urlParams;
const { data = INITIAL_DATA } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
index 0ad221b95b4ff..9c3a18b9c0d0d 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IUrlParams } from '../context/UrlParamsContext/types';
+import { useParams } from 'react-router-dom';
import { useUiFilters } from '../context/UrlParamsContext';
-import { useFetcher } from './useFetcher';
+import { IUrlParams } from '../context/UrlParamsContext/types';
import { APIReturnType } from '../services/rest/createCallApmApi';
+import { useFetcher } from './useFetcher';
type TransactionsAPIResponse = APIReturnType<
'/api/apm/services/{serviceName}/transaction_groups'
@@ -20,7 +21,8 @@ const DEFAULT_RESPONSE: TransactionsAPIResponse = {
};
export function useTransactionList(urlParams: IUrlParams) {
- const { serviceName, transactionType, start, end } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { transactionType, start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = DEFAULT_RESPONSE, error, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
similarity index 65%
rename from x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx
rename to x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
index 102a3d91e4a91..dcd6ed0ba4934 100644
--- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx
+++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
@@ -4,63 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { mount } from 'enzyme';
-import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import produce from 'immer';
+import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
-import { ApmPluginContextValue } from '../../../context/ApmPluginContext';
-import { routes } from './route_config';
-import { UpdateBreadcrumbs } from './UpdateBreadcrumbs';
+import { routes } from '../components/app/Main/route_config';
+import { ApmPluginContextValue } from '../context/ApmPluginContext';
import {
- MockApmPluginContextWrapper,
mockApmPluginContextValue,
-} from '../../../context/ApmPluginContext/MockApmPluginContext';
+ MockApmPluginContextWrapper,
+} from '../context/ApmPluginContext/MockApmPluginContext';
+import { useBreadcrumbs } from './use_breadcrumbs';
-const setBreadcrumbs = jest.fn();
-const changeTitle = jest.fn();
+function createWrapper(path: string) {
+ return ({ children }: { children?: ReactNode }) => {
+ const value = (produce(mockApmPluginContextValue, (draft) => {
+ draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
+ draft.core.chrome.docTitle.change = changeTitle;
+ draft.core.chrome.setBreadcrumbs = setBreadcrumbs;
+ }) as unknown) as ApmPluginContextValue;
-function mountBreadcrumb(route: string, params = '') {
- mount(
-
-
-
+ return (
+
+
+ {children}
+
-
- );
- expect(setBreadcrumbs).toHaveBeenCalledTimes(1);
+ );
+ };
}
-describe('UpdateBreadcrumbs', () => {
- beforeEach(() => {
- setBreadcrumbs.mockReset();
- changeTitle.mockReset();
- });
+function mountBreadcrumb(path: string) {
+ renderHook(() => useBreadcrumbs(routes), { wrapper: createWrapper(path) });
+}
- it('Changes the homepage title', () => {
+const changeTitle = jest.fn();
+const setBreadcrumbs = jest.fn();
+
+describe('useBreadcrumbs', () => {
+ it('changes the page title', () => {
mountBreadcrumb('/');
+
expect(changeTitle).toHaveBeenCalledWith(['APM']);
});
- it('/services/:serviceName/errors/:groupId', () => {
+ test('/services/:serviceName/errors/:groupId', () => {
mountBreadcrumb(
- '/services/opbeans-node/errors/myGroupId',
- 'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
+ '/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
);
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -95,10 +88,10 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/errors', () => {
- mountBreadcrumb('/services/opbeans-node/errors');
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ test('/services/:serviceName/errors', () => {
+ mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -115,6 +108,7 @@ describe('UpdateBreadcrumbs', () => {
expect.objectContaining({ text: 'Errors', href: undefined }),
])
);
+
expect(changeTitle).toHaveBeenCalledWith([
'Errors',
'opbeans-node',
@@ -123,10 +117,10 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/transactions', () => {
- mountBreadcrumb('/services/opbeans-node/transactions');
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ test('/services/:serviceName/transactions', () => {
+ mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -152,14 +146,12 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
+ test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
mountBreadcrumb(
- '/services/opbeans-node/transactions/view',
- 'transactionName=my-transaction-name'
+ '/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
);
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
new file mode 100644
index 0000000000000..640170bf3bff2
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { History, Location } from 'history';
+import { ChromeBreadcrumb } from 'kibana/public';
+import { MouseEvent, ReactNode, useEffect } from 'react';
+import {
+ matchPath,
+ RouteComponentProps,
+ useHistory,
+ match as Match,
+ useLocation,
+} from 'react-router-dom';
+import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
+import { getAPMHref } from '../components/shared/Links/apm/APMLink';
+import { useApmPluginContext } from './useApmPluginContext';
+
+interface BreadcrumbWithoutLink extends ChromeBreadcrumb {
+ match: Match>;
+}
+
+interface BreadcrumbFunctionArgs extends RouteComponentProps {
+ breadcrumbTitle: BreadcrumbTitle;
+}
+
+/**
+ * Call the breadcrumb function if there is one, otherwise return it as a string
+ */
+function getBreadcrumbText({
+ breadcrumbTitle,
+ history,
+ location,
+ match,
+}: BreadcrumbFunctionArgs) {
+ return typeof breadcrumbTitle === 'function'
+ ? breadcrumbTitle({ history, location, match })
+ : breadcrumbTitle;
+}
+
+/**
+ * Get a breadcrumb from the current path and route definitions.
+ */
+function getBreadcrumb({
+ currentPath,
+ history,
+ location,
+ routes,
+}: {
+ currentPath: string;
+ history: History;
+ location: Location;
+ routes: APMRouteDefinition[];
+}) {
+ return routes.reduce(
+ (found, { breadcrumb, ...routeDefinition }) => {
+ if (found) {
+ return found;
+ }
+
+ if (!breadcrumb) {
+ return null;
+ }
+
+ const match = matchPath>(
+ currentPath,
+ routeDefinition
+ );
+
+ if (match) {
+ return {
+ match,
+ text: getBreadcrumbText({
+ breadcrumbTitle: breadcrumb,
+ history,
+ location,
+ match,
+ }),
+ };
+ }
+
+ return null;
+ },
+ null
+ );
+}
+
+/**
+ * Once we have the breadcrumbs, we need to iterate through the list again to
+ * add the href and onClick, since we need to know which one is the final
+ * breadcrumb
+ */
+function addLinksToBreadcrumbs({
+ breadcrumbs,
+ navigateToUrl,
+ wrappedGetAPMHref,
+}: {
+ breadcrumbs: BreadcrumbWithoutLink[];
+ navigateToUrl: (url: string) => Promise;
+ wrappedGetAPMHref: (path: string) => string;
+}) {
+ return breadcrumbs.map((breadcrumb, index) => {
+ const isLastBreadcrumbItem = index === breadcrumbs.length - 1;
+
+ // Make the link not clickable if it's the last item
+ const href = isLastBreadcrumbItem
+ ? undefined
+ : wrappedGetAPMHref(breadcrumb.match.url);
+ const onClick = !href
+ ? undefined
+ : (event: MouseEvent) => {
+ event.preventDefault();
+ navigateToUrl(href);
+ };
+
+ return {
+ ...breadcrumb,
+ match: undefined,
+ href,
+ onClick,
+ };
+ });
+}
+
+/**
+ * Convert a list of route definitions to a list of breadcrumbs
+ */
+function routeDefinitionsToBreadcrumbs({
+ history,
+ location,
+ routes,
+}: {
+ history: History;
+ location: Location;
+ routes: APMRouteDefinition[];
+}) {
+ const breadcrumbs: BreadcrumbWithoutLink[] = [];
+ const { pathname } = location;
+
+ pathname
+ .split('?')[0]
+ .replace(/\/$/, '')
+ .split('/')
+ .reduce((acc, next) => {
+ // `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
+ const currentPath = !next ? '/' : `${acc}/${next}`;
+ const breadcrumb = getBreadcrumb({
+ currentPath,
+ history,
+ location,
+ routes,
+ });
+
+ if (breadcrumb) {
+ breadcrumbs.push(breadcrumb);
+ }
+
+ return currentPath === '/' ? '' : currentPath;
+ }, '');
+
+ return breadcrumbs;
+}
+
+/**
+ * Get an array for a page title from a list of breadcrumbs
+ */
+function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] {
+ function removeNonStrings(item: ReactNode): item is string {
+ return typeof item === 'string';
+ }
+
+ return breadcrumbs
+ .map(({ text }) => text)
+ .reverse()
+ .filter(removeNonStrings);
+}
+
+/**
+ * Determine the breadcrumbs from the routes, set them, and update the page
+ * title when the route changes.
+ */
+export function useBreadcrumbs(routes: APMRouteDefinition[]) {
+ const history = useHistory();
+ const location = useLocation();
+ const { search } = location;
+ const { core } = useApmPluginContext();
+ const { basePath } = core.http;
+ const { navigateToUrl } = core.application;
+ const { docTitle, setBreadcrumbs } = core.chrome;
+ const changeTitle = docTitle.change;
+
+ function wrappedGetAPMHref(path: string) {
+ return getAPMHref({ basePath, path, search });
+ }
+
+ const breadcrumbsWithoutLinks = routeDefinitionsToBreadcrumbs({
+ history,
+ location,
+ routes,
+ });
+ const breadcrumbs = addLinksToBreadcrumbs({
+ breadcrumbs: breadcrumbsWithoutLinks,
+ wrappedGetAPMHref,
+ navigateToUrl,
+ });
+ const title = getTitleFromBreadcrumbs(breadcrumbs);
+
+ useEffect(() => {
+ changeTitle(title);
+ setBreadcrumbs(breadcrumbs);
+ }, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]);
+}
diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md
index 9b02972d35302..d6fdb5f52291c 100644
--- a/x-pack/plugins/apm/readme.md
+++ b/x-pack/plugins/apm/readme.md
@@ -162,4 +162,5 @@ You can access the development environment at http://localhost:9001.
- [Cypress integration tests](./e2e/README.md)
- [VSCode setup instructions](./dev_docs/vscode_setup.md)
- [Github PR commands](./dev_docs/github_commands.md)
+- [Routing and Linking](./dev_docs/routing_and_linking.md)
- [Telemetry](./dev_docs/telemetry.md)
diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
index c3cf363cbec05..ef85112918712 100644
--- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
+++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
@@ -4,15 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Client } from '@elastic/elasticsearch';
import { argv } from 'yargs';
import pLimit from 'p-limit';
import pRetry from 'p-retry';
-import { parse, format } from 'url';
import { set } from '@elastic/safer-lodash-set';
import { uniq, without, merge, flatten } from 'lodash';
import * as histogram from 'hdr-histogram-js';
-import { ESSearchResponse } from '../../typings/elasticsearch';
import {
HOST_NAME,
SERVICE_NAME,
@@ -28,6 +25,8 @@ import {
} from '../../common/elasticsearch_fieldnames';
import { stampLogger } from '../shared/stamp-logger';
import { createOrUpdateIndex } from '../shared/create-or-update-index';
+import { parseIndexUrl } from '../shared/parse_index_url';
+import { ESClient, getEsClient } from '../shared/get_es_client';
// This script will try to estimate how many latency metric documents
// will be created based on the available transaction documents.
@@ -125,41 +124,18 @@ export async function aggregateLatencyMetrics() {
const source = String(argv.source ?? '');
const dest = String(argv.dest ?? '');
- function getClientOptionsFromIndexUrl(
- url: string
- ): { node: string; index: string } {
- const parsed = parse(url);
- const { pathname, ...rest } = parsed;
+ const sourceOptions = parseIndexUrl(source);
- return {
- node: format(rest),
- index: pathname!.replace('/', ''),
- };
- }
-
- const sourceOptions = getClientOptionsFromIndexUrl(source);
-
- const sourceClient = new Client({
- node: sourceOptions.node,
- ssl: {
- rejectUnauthorized: false,
- },
- requestTimeout: 120000,
- });
+ const sourceClient = getEsClient({ node: sourceOptions.node });
- let destClient: Client | undefined;
+ let destClient: ESClient | undefined;
let destOptions: { node: string; index: string } | undefined;
const uploadMetrics = !!dest;
if (uploadMetrics) {
- destOptions = getClientOptionsFromIndexUrl(dest);
- destClient = new Client({
- node: destOptions.node,
- ssl: {
- rejectUnauthorized: false,
- },
- });
+ destOptions = parseIndexUrl(dest);
+ destClient = getEsClient({ node: destOptions.node });
const mappings = (
await sourceClient.indices.getMapping({
@@ -298,10 +274,9 @@ export async function aggregateLatencyMetrics() {
},
};
- const response = (await sourceClient.search(params))
- .body as ESSearchResponse;
+ const response = await sourceClient.search(params);
- const { aggregations } = response;
+ const { aggregations } = response.body;
if (!aggregations) {
return buckets;
@@ -333,10 +308,9 @@ export async function aggregateLatencyMetrics() {
},
};
- const response = (await sourceClient.search(params))
- .body as ESSearchResponse;
+ const response = await sourceClient.search(params);
- return response.hits.total.value;
+ return response.body.hits.total.value;
}
const [buckets, numberOfTransactionDocuments] = await Promise.all([
diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive.js b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js
new file mode 100644
index 0000000000000..6b3473dc2ac0a
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// compile typescript on the fly
+// eslint-disable-next-line import/no-extraneous-dependencies
+require('@babel/register')({
+ extensions: ['.js', '.ts'],
+ plugins: ['@babel/plugin-proposal-optional-chaining'],
+ presets: [
+ '@babel/typescript',
+ ['@babel/preset-env', { targets: { node: 'current' } }],
+ ],
+});
+
+require('./create-functional-tests-archive/index.ts');
diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts
new file mode 100644
index 0000000000000..cbd63262bd08d
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { argv } from 'yargs';
+import { execSync } from 'child_process';
+import moment from 'moment';
+import path from 'path';
+import fs from 'fs';
+import { stampLogger } from '../shared/stamp-logger';
+
+async function run() {
+ stampLogger();
+
+ const archiveName = 'apm_8.0.0';
+
+ // include important APM data and ML data
+ const indices =
+ 'apm-*-transaction,apm-*-span,apm-*-error,apm-*-metric,.ml-anomalies*,.ml-config';
+
+ const esUrl = argv['es-url'] as string | undefined;
+
+ if (!esUrl) {
+ throw new Error('--es-url is not set');
+ }
+ const kibanaUrl = argv['kibana-url'] as string | undefined;
+
+ if (!kibanaUrl) {
+ throw new Error('--kibana-url is not set');
+ }
+ const gte = moment().subtract(1, 'hour').toISOString();
+ const lt = moment(gte).add(30, 'minutes').toISOString();
+
+ // eslint-disable-next-line no-console
+ console.log(`Archiving from ${gte} to ${lt}...`);
+
+ // APM data uses '@timestamp' (ECS), ML data uses 'timestamp'
+
+ const rangeQueries = [
+ {
+ range: {
+ '@timestamp': {
+ gte,
+ lt,
+ },
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ gte,
+ lt,
+ },
+ },
+ },
+ ];
+
+ // some of the data is timeless/content
+ const query = {
+ bool: {
+ should: [
+ ...rangeQueries,
+ {
+ bool: {
+ must_not: [
+ {
+ exists: {
+ field: '@timestamp',
+ },
+ },
+ {
+ exists: {
+ field: 'timestamp',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ };
+
+ const archivesDir = path.join(__dirname, '.archives');
+ const root = path.join(__dirname, '../../../../..');
+
+ // create the archive
+
+ execSync(
+ `node scripts/es_archiver save ${archiveName} ${indices} --dir=${archivesDir} --kibana-url=${kibanaUrl} --es-url=${esUrl} --query='${JSON.stringify(
+ query
+ )}'`,
+ {
+ cwd: root,
+ stdio: 'inherit',
+ }
+ );
+
+ const targetDirs = ['trial', 'basic'];
+
+ // copy the archives to the test fixtures
+
+ await Promise.all(
+ targetDirs.map(async (target) => {
+ const targetPath = path.resolve(
+ __dirname,
+ '../../../../test/apm_api_integration/',
+ target
+ );
+ const targetArchivesPath = path.resolve(
+ targetPath,
+ 'fixtures/es_archiver',
+ archiveName
+ );
+
+ if (!fs.existsSync(targetArchivesPath)) {
+ fs.mkdirSync(targetArchivesPath);
+ }
+
+ fs.copyFileSync(
+ path.join(archivesDir, archiveName, 'data.json.gz'),
+ path.join(targetArchivesPath, 'data.json.gz')
+ );
+ fs.copyFileSync(
+ path.join(archivesDir, archiveName, 'mappings.json'),
+ path.join(targetArchivesPath, 'mappings.json')
+ );
+
+ const currentConfig = {};
+
+ // get the current metadata and extend/override metadata for the new archive
+ const configFilePath = path.join(targetPath, 'archives_metadata.ts');
+
+ try {
+ Object.assign(currentConfig, (await import(configFilePath)).default);
+ } catch (error) {
+ // do nothing
+ }
+
+ const newConfig = {
+ ...currentConfig,
+ [archiveName]: {
+ start: gte,
+ end: lt,
+ },
+ };
+
+ fs.writeFileSync(
+ configFilePath,
+ `export default ${JSON.stringify(newConfig, null, 2)}`,
+ { encoding: 'utf-8' }
+ );
+ })
+ );
+
+ fs.unlinkSync(path.join(archivesDir, archiveName, 'data.json.gz'));
+ fs.unlinkSync(path.join(archivesDir, archiveName, 'mappings.json'));
+ fs.rmdirSync(path.join(archivesDir, archiveName));
+ fs.rmdirSync(archivesDir);
+
+ // run ESLint on the generated metadata files
+
+ execSync('node scripts/eslint **/*/archives_metadata.ts --fix', {
+ cwd: root,
+ stdio: 'inherit',
+ });
+}
+
+run()
+ .then(() => {
+ process.exit(0);
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.log(err);
+ process.exit(1);
+ });
diff --git a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
index 6d44e12fb00a2..01fa5b0509bcd 100644
--- a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
+++ b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Client } from '@elastic/elasticsearch';
+import { ESClient } from './get_es_client';
export async function createOrUpdateIndex({
client,
@@ -12,7 +12,7 @@ export async function createOrUpdateIndex({
indexName,
template,
}: {
- client: Client;
+ client: ESClient;
clear: boolean;
indexName: string;
template: any;
diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts
new file mode 100644
index 0000000000000..86dfd92190fdf
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Client } from '@elastic/elasticsearch';
+import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool';
+import { ESSearchResponse, ESSearchRequest } from '../../typings/elasticsearch';
+
+export type ESClient = ReturnType;
+
+export function getEsClient({
+ node,
+ auth,
+}: {
+ node: string;
+ auth?: BasicAuth | ApiKeyAuth;
+}) {
+ const client = new Client({
+ node,
+ ssl: {
+ rejectUnauthorized: false,
+ },
+ requestTimeout: 120000,
+ auth,
+ });
+
+ return {
+ ...client,
+ async search(
+ request: TSearchRequest
+ ) {
+ const response = await client.search(request as any);
+
+ return {
+ ...response,
+ body: response.body as ESSearchResponse,
+ };
+ },
+ };
+}
diff --git a/x-pack/plugins/apm/scripts/shared/parse_index_url.ts b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts
new file mode 100644
index 0000000000000..190f7fda396bd
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { parse, format } from 'url';
+
+export function parseIndexUrl(url: string): { node: string; index: string } {
+ const parsed = parse(url);
+ const { pathname, ...rest } = parsed;
+
+ return {
+ node: format(rest),
+ index: pathname!.replace('/', ''),
+ };
+}
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
new file mode 100644
index 0000000000000..9395e5fe14336
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getRumOverviewProjection } from '../../projections/rum_overview';
+import { mergeProjection } from '../../projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+import {
+ CLS_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
+} from '../../../common/elasticsearch_fieldnames';
+
+export async function getWebCoreVitals({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...projection.body.query.bool.filter,
+ {
+ term: {
+ 'user_agent.name': 'Chrome',
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ lcp: {
+ percentiles: {
+ field: LCP_FIELD,
+ percents: [50],
+ },
+ },
+ fid: {
+ percentiles: {
+ field: FID_FIELD,
+ percents: [50],
+ },
+ },
+ cls: {
+ percentiles: {
+ field: CLS_FIELD,
+ percents: [50],
+ },
+ },
+ lcpRanks: {
+ percentile_ranks: {
+ field: LCP_FIELD,
+ values: [2500, 4000],
+ keyed: false,
+ },
+ },
+ fidRanks: {
+ percentile_ranks: {
+ field: FID_FIELD,
+ values: [100, 300],
+ keyed: false,
+ },
+ },
+ clsRanks: {
+ percentile_ranks: {
+ field: CLS_FIELD,
+ values: [0.1, 0.25],
+ keyed: false,
+ },
+ },
+ },
+ },
+ });
+
+ const { apmEventClient } = setup;
+
+ const response = await apmEventClient.search(params);
+ const {
+ lcp,
+ cls,
+ fid,
+ lcpRanks,
+ fidRanks,
+ clsRanks,
+ } = response.aggregations!;
+
+ const getRanksPercentages = (
+ ranks: Array<{ key: number; value: number }>
+ ) => {
+ const ranksVal = (ranks ?? [0, 0]).map(
+ ({ value }) => value?.toFixed(0) ?? 0
+ );
+ return [
+ Number(ranksVal?.[0]),
+ Number(ranksVal?.[1]) - Number(ranksVal?.[0]),
+ 100 - Number(ranksVal?.[1]),
+ ];
+ };
+
+ // Divide by 1000 to convert ms into seconds
+ return {
+ cls: String(cls.values['50.0'] || 0),
+ fid: ((fid.values['50.0'] || 0) / 1000).toFixed(2),
+ lcp: ((lcp.values['50.0'] || 0) / 1000).toFixed(2),
+
+ lcpRanks: getRanksPercentages(lcpRanks.values),
+ fidRanks: getRanksPercentages(fidRanks.values),
+ clsRanks: getRanksPercentages(clsRanks.values),
+ };
+}
diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
index 2f3b2a602048c..926b2025f4253 100644
--- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
@@ -5,7 +5,7 @@
*/
import { merge } from 'lodash';
-import { Server } from 'hapi';
+
import { SavedObjectsClient } from 'src/core/server';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import {
@@ -32,10 +32,6 @@ export interface ApmIndicesConfig {
export type ApmIndicesName = keyof ApmIndicesConfig;
-export type ScopedSavedObjectsClient = ReturnType<
- Server['savedObjects']['getScopedSavedObjectsClient']
->;
-
async function getApmIndicesSavedObject(
savedObjectsClient: ISavedObjectsClient
) {
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index 5dff13e5b37e0..cf7a02cde975c 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -77,6 +77,7 @@ import {
rumPageLoadDistBreakdownRoute,
rumServicesRoute,
rumVisitorsBreakdownRoute,
+ rumWebCoreVitals,
} from './rum_client';
import {
observabilityOverviewHasDataRoute,
@@ -172,6 +173,7 @@ const createApmApi = () => {
.add(rumClientMetricsRoute)
.add(rumServicesRoute)
.add(rumVisitorsBreakdownRoute)
+ .add(rumWebCoreVitals)
// Observability dashboard
.add(observabilityOverviewHasDataRoute)
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
index 0781512c6f7a0..e17791f56eef2 100644
--- a/x-pack/plugins/apm/server/routes/rum_client.ts
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -14,6 +14,7 @@ import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distrib
import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown';
import { getRumServices } from '../lib/rum_client/get_rum_services';
import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown';
+import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@@ -117,3 +118,15 @@ export const rumVisitorsBreakdownRoute = createRoute(() => ({
return getVisitorBreakdown({ setup });
},
}));
+
+export const rumWebCoreVitals = createRoute(() => ({
+ path: '/api/apm/rum-client/web-core-vitals',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+
+ return getWebCoreVitals({ setup });
+ },
+}));
diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts
index 97013273c9bcf..78c820fbf4ecd 100644
--- a/x-pack/plugins/apm/server/routes/typings.ts
+++ b/x-pack/plugins/apm/server/routes/typings.ts
@@ -13,7 +13,6 @@ import {
} from 'src/core/server';
import { PickByValue, Optional } from 'utility-types';
import { Observable } from 'rxjs';
-import { Server } from 'hapi';
import { ObservabilityPluginSetup } from '../../../observability/server';
import { SecurityPluginSetup } from '../../../security/server';
import { MlPluginSetup } from '../../../ml/server';
@@ -57,12 +56,6 @@ export interface Route<
}) => Promise;
}
-export type APMLegacyServer = Pick & {
- plugins: {
- elasticsearch: Server['plugins']['elasticsearch'];
- };
-};
-
export type APMRequestHandlerContext<
TDecodedParams extends { [key in keyof Params]: any } = {}
> = RequestHandlerContext & {
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index f957614122547..7a7592b248960 100644
--- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -146,7 +146,7 @@ export interface AggregationOptionsByType {
buckets: number;
} & AggregationSourceOptions;
percentile_ranks: {
- values: string[];
+ values: Array;
keyed?: boolean;
hdr?: { number_of_significant_value_digits: number };
} & AggregationSourceOptions;
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index bd12c258a5388..15a318002390f 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -28,5 +28,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
+export const JIRA_ACTION_TYPE_ID = '.jira';
+export const RESILIENT_ACTION_TYPE_ID = '.resilient';
-export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient'];
+export const SUPPORTED_CONNECTORS = [
+ SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
+ RESILIENT_ACTION_TYPE_ID,
+];
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
index 28e75dd2f8c32..a22d7ae5cea21 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
@@ -12,6 +12,7 @@ import {
CASE_CONFIGURE_CONNECTORS_URL,
SUPPORTED_CONNECTORS,
SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
} from '../../../../../common/constants';
/*
@@ -36,8 +37,9 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
(action) =>
SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
// Need this filtering temporary to display only Case owned ServiceNow connectors
- (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID ||
- (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned))
+ (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) ||
+ ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) &&
+ action.config?.isCaseOwned === true))
);
return response.ok({ body: results });
} catch (error) {
diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts
index d6a3c73aaf363..012f1204da46a 100644
--- a/x-pack/plugins/data_enhanced/common/index.ts
+++ b/x-pack/plugins/data_enhanced/common/index.ts
@@ -5,7 +5,6 @@
*/
export {
- EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
ENHANCED_ES_SEARCH_STRATEGY,
diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts
index 2ae422bd6b7d7..696938a403e89 100644
--- a/x-pack/plugins/data_enhanced/common/search/index.ts
+++ b/x-pack/plugins/data_enhanced/common/search/index.ts
@@ -5,7 +5,6 @@
*/
export {
- EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
ENHANCED_ES_SEARCH_STRATEGY,
diff --git a/x-pack/plugins/data_enhanced/common/search/types.ts b/x-pack/plugins/data_enhanced/common/search/types.ts
index 0d3d3a69e1e57..24d459ade4bf9 100644
--- a/x-pack/plugins/data_enhanced/common/search/types.ts
+++ b/x-pack/plugins/data_enhanced/common/search/types.ts
@@ -4,21 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IEsSearchRequest, ISearchRequestParams } from '../../../../../src/plugins/data/common';
+import { IEsSearchRequest } from '../../../../../src/plugins/data/common';
export const ENHANCED_ES_SEARCH_STRATEGY = 'ese';
-export interface EnhancedSearchParams extends ISearchRequestParams {
- ignoreThrottled: boolean;
-}
-
export interface IAsyncSearchRequest extends IEsSearchRequest {
/**
* The ID received from the response from the initial request
*/
id?: string;
-
- params?: EnhancedSearchParams;
}
export interface IEnhancedEsSearchRequest extends IEsSearchRequest {
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index 7f6e3feac0671..ccc93316482c2 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -23,6 +23,8 @@ export type DataEnhancedStart = ReturnType;
export class DataEnhancedPlugin
implements Plugin {
+ private enhancedSearchInterceptor!: EnhancedSearchInterceptor;
+
public setup(
core: CoreSetup,
{ data }: DataEnhancedSetupDependencies
@@ -32,20 +34,17 @@ export class DataEnhancedPlugin
setupKqlQuerySuggestionProvider(core)
);
- const enhancedSearchInterceptor = new EnhancedSearchInterceptor(
- {
- toasts: core.notifications.toasts,
- http: core.http,
- uiSettings: core.uiSettings,
- startServices: core.getStartServices(),
- usageCollector: data.search.usageCollector,
- },
- core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
- );
+ this.enhancedSearchInterceptor = new EnhancedSearchInterceptor({
+ toasts: core.notifications.toasts,
+ http: core.http,
+ uiSettings: core.uiSettings,
+ startServices: core.getStartServices(),
+ usageCollector: data.search.usageCollector,
+ });
data.__enhance({
search: {
- searchInterceptor: enhancedSearchInterceptor,
+ searchInterceptor: this.enhancedSearchInterceptor,
},
});
}
@@ -53,4 +52,8 @@ export class DataEnhancedPlugin
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
setAutocompleteService(plugins.data.autocomplete);
}
+
+ public stop() {
+ this.enhancedSearchInterceptor.stop();
+ }
}
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
index 1e2c7987b7041..261e03887acdb 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
@@ -7,7 +7,7 @@
import { coreMock } from '../../../../../src/core/public/mocks';
import { EnhancedSearchInterceptor } from './search_interceptor';
import { CoreSetup, CoreStart } from 'kibana/public';
-import { AbortError } from '../../../../../src/plugins/data/common';
+import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common';
const timeTravel = (msToRun = 0) => {
jest.advanceTimersByTime(msToRun);
@@ -43,6 +43,15 @@ describe('EnhancedSearchInterceptor', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
+ mockCoreSetup.uiSettings.get.mockImplementation((name: string) => {
+ switch (name) {
+ case UI_SETTINGS.SEARCH_TIMEOUT:
+ return 1000;
+ default:
+ return;
+ }
+ });
+
next.mockClear();
error.mockClear();
complete.mockClear();
@@ -64,16 +73,13 @@ describe('EnhancedSearchInterceptor', () => {
]);
});
- searchInterceptor = new EnhancedSearchInterceptor(
- {
- toasts: mockCoreSetup.notifications.toasts,
- startServices: mockPromise as any,
- http: mockCoreSetup.http,
- uiSettings: mockCoreSetup.uiSettings,
- usageCollector: mockUsageCollector,
- },
- 1000
- );
+ searchInterceptor = new EnhancedSearchInterceptor({
+ toasts: mockCoreSetup.notifications.toasts,
+ startServices: mockPromise as any,
+ http: mockCoreSetup.http,
+ uiSettings: mockCoreSetup.uiSettings,
+ usageCollector: mockUsageCollector,
+ });
});
describe('search', () => {
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
index 6f7899d1188b4..61cf579d3136b 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { throwError, EMPTY, timer, from } from 'rxjs';
+import { throwError, EMPTY, timer, from, Subscription } from 'rxjs';
import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators';
import { getLongQueryNotification } from './long_query_notification';
import {
@@ -17,14 +17,25 @@ import { IAsyncSearchOptions } from '.';
import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common';
export class EnhancedSearchInterceptor extends SearchInterceptor {
+ private uiSettingsSub: Subscription;
+ private searchTimeout: number;
+
/**
- * This class should be instantiated with a `requestTimeout` corresponding with how many ms after
- * requests are initiated that they should automatically cancel.
- * @param deps `SearchInterceptorDeps`
- * @param requestTimeout Usually config value `elasticsearch.requestTimeout`
+ * @internal
*/
- constructor(deps: SearchInterceptorDeps, requestTimeout?: number) {
- super(deps, requestTimeout);
+ constructor(deps: SearchInterceptorDeps) {
+ super(deps);
+ this.searchTimeout = deps.uiSettings.get(UI_SETTINGS.SEARCH_TIMEOUT);
+
+ this.uiSettingsSub = deps.uiSettings
+ .get$(UI_SETTINGS.SEARCH_TIMEOUT)
+ .subscribe((timeout: number) => {
+ this.searchTimeout = timeout;
+ });
+ }
+
+ public stop() {
+ this.uiSettingsSub.unsubscribe();
}
/**
@@ -69,12 +80,10 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
) {
let { id } = request;
- request.params = {
- ignoreThrottled: !this.deps.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
- ...request.params,
- };
-
- const { combinedSignal, cleanup } = this.setupTimers(options);
+ const { combinedSignal, cleanup } = this.setupAbortSignal({
+ abortSignal: options.abortSignal,
+ timeout: this.searchTimeout,
+ });
const aborted$ = from(toPromise(combinedSignal));
const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY;
@@ -108,7 +117,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
- this.deps.http.delete(`/internal/search/es/${id}`);
+ this.deps.http.delete(`/internal/search/${strategy}/${id}`);
}
},
}),
diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts
index f9b6fd4e9ad64..3b05e83d208b7 100644
--- a/x-pack/plugins/data_enhanced/server/plugin.ts
+++ b/x-pack/plugins/data_enhanced/server/plugin.ts
@@ -19,6 +19,7 @@ import {
import { enhancedEsSearchStrategyProvider } from './search';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
+import { getUiSettings } from './ui_settings';
interface SetupDependencies {
data: DataPluginSetup;
@@ -35,6 +36,8 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) {
const usage = deps.usageCollection ? usageProvider(core) : undefined;
+ core.uiSettings.register(getUiSettings());
+
deps.data.search.registerSearchStrategy(
ENHANCED_ES_SEARCH_STRATEGY,
enhancedEsSearchStrategyProvider(
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index a287f72ca9161..f4f3d894a4576 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -5,8 +5,8 @@
*/
import { RequestHandlerContext } from '../../../../../src/core/server';
-import { pluginInitializerContextConfigMock } from '../../../../../src/core/server/mocks';
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
+import { BehaviorSubject } from 'rxjs';
const mockAsyncResponse = {
body: {
@@ -42,6 +42,11 @@ describe('ES search strategy', () => {
};
const mockContext = {
core: {
+ uiSettings: {
+ client: {
+ get: jest.fn(),
+ },
+ },
elasticsearch: {
client: {
asCurrentUser: {
@@ -55,7 +60,15 @@ describe('ES search strategy', () => {
},
},
};
- const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
+ const mockConfig$ = new BehaviorSubject({
+ elasticsearch: {
+ shardTimeout: {
+ asMilliseconds: () => {
+ return 100;
+ },
+ },
+ },
+ });
beforeEach(() => {
mockApiCaller.mockClear();
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 4ace1c4c5385b..eda6178dc8e5b 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -5,23 +5,19 @@
*/
import { first } from 'rxjs/operators';
-import { mapKeys, snakeCase } from 'lodash';
-import { Observable } from 'rxjs';
import { SearchResponse } from 'elasticsearch';
+import { Observable } from 'rxjs';
+import { SharedGlobalConfig, RequestHandlerContext, Logger } from '../../../../../src/core/server';
import {
- SharedGlobalConfig,
- RequestHandlerContext,
- ElasticsearchClient,
- Logger,
-} from '../../../../../src/core/server';
-import {
- getDefaultSearchParams,
getTotalLoaded,
ISearchStrategy,
SearchUsage,
+ getDefaultSearchParams,
+ getShardTimeout,
+ toSnakeCase,
+ shimHitsTotal,
} from '../../../../../src/plugins/data/server';
import { IEnhancedEsSearchRequest } from '../../common';
-import { shimHitsTotal } from './shim_hits_total';
import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search';
function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse {
@@ -39,17 +35,13 @@ export const enhancedEsSearchStrategyProvider = (
options?: ISearchOptions
) => {
logger.debug(`search ${JSON.stringify(request.params) || request.id}`);
- const config = await config$.pipe(first()).toPromise();
- const client = context.core.elasticsearch.client.asCurrentUser;
- const defaultParams = getDefaultSearchParams(config);
- const params = { ...defaultParams, ...request.params };
const isAsync = request.indexType !== 'rollup';
try {
const response = isAsync
- ? await asyncSearch(client, { ...request, params }, options)
- : await rollupSearch(client, { ...request, params }, options);
+ ? await asyncSearch(context, request)
+ : await rollupSearch(context, request);
if (
usage &&
@@ -75,72 +67,75 @@ export const enhancedEsSearchStrategyProvider = (
});
};
- return { search, cancel };
-};
-
-async function asyncSearch(
- client: ElasticsearchClient,
- request: IEnhancedEsSearchRequest,
- options?: ISearchOptions
-): Promise {
- let esResponse;
+ async function asyncSearch(
+ context: RequestHandlerContext,
+ request: IEnhancedEsSearchRequest
+ ): Promise {
+ let esResponse;
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ const uiSettingsClient = await context.core.uiSettings.client;
+
+ const asyncOptions = {
+ waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return
+ keepAlive: '1m', // Extend the TTL for this search request by one minute
+ };
+
+ // If we have an ID, then just poll for that ID, otherwise send the entire request body
+ if (!request.id) {
+ const submitOptions = toSnakeCase({
+ batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results
+ ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...asyncOptions,
+ ...request.params,
+ });
+
+ esResponse = await esClient.asyncSearch.submit(submitOptions);
+ } else {
+ esResponse = await esClient.asyncSearch.get({
+ id: request.id,
+ ...toSnakeCase(asyncOptions),
+ });
+ }
- const asyncOptions = {
- waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return
- keepAlive: '1m', // Extend the TTL for this search request by one minute
- };
+ const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body;
+ return {
+ id,
+ isPartial,
+ isRunning,
+ rawResponse: shimHitsTotal(response),
+ ...getTotalLoaded(response._shards),
+ };
+ }
- // If we have an ID, then just poll for that ID, otherwise send the entire request body
- if (!request.id) {
- const submitOptions = toSnakeCase({
- batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results
- trackTotalHits: true, // Get the exact count of hits
- ...asyncOptions,
- ...request.params,
+ const rollupSearch = async function (
+ context: RequestHandlerContext,
+ request: IEnhancedEsSearchRequest
+ ): Promise {
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ const uiSettingsClient = await context.core.uiSettings.client;
+ const config = await config$.pipe(first()).toPromise();
+ const { body, index, ...params } = request.params!;
+ const method = 'POST';
+ const path = encodeURI(`/${index}/_rollup_search`);
+ const querystring = toSnakeCase({
+ ...getShardTimeout(config),
+ ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...params,
});
- esResponse = await client.asyncSearch.submit(submitOptions);
- } else {
- esResponse = await client.asyncSearch.get({
- id: request.id,
- ...toSnakeCase(asyncOptions),
+ const esResponse = await esClient.transport.request({
+ method,
+ path,
+ body,
+ querystring,
});
- }
-
- const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body;
- return {
- id,
- isPartial,
- isRunning,
- rawResponse: shimHitsTotal(response),
- ...getTotalLoaded(response._shards),
- };
-}
-async function rollupSearch(
- client: ElasticsearchClient,
- request: IEnhancedEsSearchRequest,
- options?: ISearchOptions
-): Promise {
- const { body, index, ...params } = request.params!;
- const method = 'POST';
- const path = encodeURI(`/${index}/_rollup_search`);
- const querystring = toSnakeCase(params);
-
- const esResponse = await client.transport.request({
- method,
- path,
- body,
- querystring,
- });
-
- const response = esResponse.body as SearchResponse;
- return {
- rawResponse: shimHitsTotal(response),
- ...getTotalLoaded(response._shards),
+ const response = esResponse.body as SearchResponse;
+ return {
+ rawResponse: response,
+ ...getTotalLoaded(response._shards),
+ };
};
-}
-function toSnakeCase(obj: Record) {
- return mapKeys(obj, (value, key) => snakeCase(key));
-}
+ return { search, cancel };
+};
diff --git a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts b/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts
deleted file mode 100644
index 10d45be01563a..0000000000000
--- a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { SearchResponse } from 'elasticsearch';
-
-/**
- * Temporary workaround until https://github.com/elastic/kibana/issues/26356 is addressed.
- * Since we are setting `track_total_hits` in the request, `hits.total` will be an object
- * containing the `value`.
- */
-export function shimHitsTotal(response: SearchResponse) {
- const total = (response.hits?.total as any)?.value ?? response.hits?.total;
- const hits = { ...response.hits, total };
- return { ...response, hits };
-}
diff --git a/x-pack/plugins/data_enhanced/server/ui_settings.ts b/x-pack/plugins/data_enhanced/server/ui_settings.ts
new file mode 100644
index 0000000000000..f2842da8b8337
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/ui_settings.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from 'kibana/server';
+import { UI_SETTINGS } from '../../../../src/plugins/data/server';
+
+export function getUiSettings(): Record> {
+ return {
+ [UI_SETTINGS.SEARCH_TIMEOUT]: {
+ name: i18n.translate('xpack.data.advancedSettings.searchTimeout', {
+ defaultMessage: 'Search Timeout',
+ }),
+ value: 600000,
+ description: i18n.translate('xpack.data.advancedSettings.searchTimeoutDesc', {
+ defaultMessage:
+ 'Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion.',
+ }),
+ type: 'number',
+ category: ['search'],
+ schema: schema.number(),
+ },
+ };
+}
diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
index 2d31be65dd30e..4533383ebd80e 100644
--- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
+++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
@@ -7,9 +7,20 @@
export const DEFAULT_INITIAL_APP_DATA = {
readOnlyMode: false,
ilmEnabled: true,
+ isFederatedAuth: false,
configuredLimits: {
- maxDocumentByteSize: 102400,
- maxEnginesPerMetaEngine: 15,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize: 102400,
+ maxEnginesPerMetaEngine: 15,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize: 102400,
+ totalFields: 64,
+ },
+ },
},
appSearch: {
accountId: 'some-id-string',
@@ -29,17 +40,16 @@ export const DEFAULT_INITIAL_APP_DATA = {
},
},
workplaceSearch: {
- canCreateInvitations: true,
- isFederatedAuth: false,
organization: {
name: 'ACME Donuts',
defaultOrgName: 'My Organization',
},
- fpAccount: {
+ account: {
id: 'some-id-string',
groups: ['Default', 'Cats'],
isAdmin: true,
canCreatePersonalSources: true,
+ canCreateInvitations: true,
isCurated: false,
viewedOnboardingPage: true,
},
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
index 05d27d7337a6e..6e2f0c0f24b7a 100644
--- a/x-pack/plugins/enterprise_search/common/constants.ts
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -11,7 +11,24 @@ export const ENTERPRISE_SEARCH_PLUGIN = {
NAME: i18n.translate('xpack.enterpriseSearch.productName', {
defaultMessage: 'Enterprise Search',
}),
- URL: '/app/enterprise_search',
+ NAV_TITLE: i18n.translate('xpack.enterpriseSearch.navTitle', {
+ defaultMessage: 'Overview',
+ }),
+ SUBTITLE: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', {
+ defaultMessage: 'Search everything',
+ }),
+ DESCRIPTIONS: [
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', {
+ defaultMessage: 'Build a powerful search experience.',
+ }),
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', {
+ defaultMessage: 'Connect your users to relevant data.',
+ }),
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', {
+ defaultMessage: 'Unify your team content.',
+ }),
+ ],
+ URL: '/app/enterprise_search/overview',
};
export const APP_SEARCH_PLUGIN = {
@@ -23,6 +40,10 @@ export const APP_SEARCH_PLUGIN = {
defaultMessage:
'Leverage dashboards, analytics, and APIs for advanced application search made simple.',
}),
+ CARD_DESCRIPTION: i18n.translate('xpack.enterpriseSearch.appSearch.productCardDescription', {
+ defaultMessage:
+ 'Elastic App Search provides user-friendly tools to design and deploy a powerful search to your websites or web/mobile applications.',
+ }),
URL: '/app/enterprise_search/app_search',
SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/',
};
@@ -36,6 +57,13 @@ export const WORKPLACE_SEARCH_PLUGIN = {
defaultMessage:
'Search all documents, files, and sources available across your virtual workplace.',
}),
+ CARD_DESCRIPTION: i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.productCardDescription',
+ {
+ defaultMessage:
+ "Unify all your team's content in one place, with instant connectivity to popular productivity and collaboration tools.",
+ }
+ ),
URL: '/app/enterprise_search/workplace_search',
SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/',
};
diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts
index 5d6ec079e66e0..72259ecd2343d 100644
--- a/x-pack/plugins/enterprise_search/common/types/app_search.ts
+++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts
@@ -23,3 +23,10 @@ export interface IRole {
availableRoleTypes: string[];
};
}
+
+export interface IConfiguredLimits {
+ engine: {
+ maxDocumentByteSize: number;
+ maxEnginesPerMetaEngine: number;
+ };
+}
diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts
index 008afb234a376..d5774adc0d516 100644
--- a/x-pack/plugins/enterprise_search/common/types/index.ts
+++ b/x-pack/plugins/enterprise_search/common/types/index.ts
@@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IAccount as IAppSearchAccount } from './app_search';
-import { IWorkplaceSearchInitialData } from './workplace_search';
+import {
+ IAccount as IAppSearchAccount,
+ IConfiguredLimits as IAppSearchConfiguredLimits,
+} from './app_search';
+import {
+ IWorkplaceSearchInitialData,
+ IConfiguredLimits as IWorkplaceSearchConfiguredLimits,
+} from './workplace_search';
export interface IInitialAppData {
readOnlyMode?: boolean;
ilmEnabled?: boolean;
+ isFederatedAuth?: boolean;
configuredLimits?: IConfiguredLimits;
+ access?: {
+ hasAppSearchAccess: boolean;
+ hasWorkplaceSearchAccess: boolean;
+ };
appSearch?: IAppSearchAccount;
workplaceSearch?: IWorkplaceSearchInitialData;
}
export interface IConfiguredLimits {
- maxDocumentByteSize: number;
- maxEnginesPerMetaEngine: number;
+ appSearch: IAppSearchConfiguredLimits;
+ workplaceSearch: IWorkplaceSearchConfiguredLimits;
}
diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
index bc4e39b0788d9..6c82206706b32 100644
--- a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
+++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
@@ -10,6 +10,7 @@ export interface IAccount {
isAdmin: boolean;
isCurated: boolean;
canCreatePersonalSources: boolean;
+ canCreateInvitations?: boolean;
viewedOnboardingPage: boolean;
}
@@ -19,8 +20,13 @@ export interface IOrganization {
}
export interface IWorkplaceSearchInitialData {
- canCreateInvitations: boolean;
- isFederatedAuth: boolean;
organization: IOrganization;
- fpAccount: IAccount;
+ account: IAccount;
+}
+
+export interface IConfiguredLimits {
+ customApiSource: {
+ maxDocumentByteSize: number;
+ totalFields: number;
+ };
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
index 779eb1a043e8c..842dcefd3aef8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
@@ -9,7 +9,7 @@
* Jest to accept its use within a jest.mock()
*/
export const mockHistory = {
- createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`),
+ createHref: jest.fn(({ pathname }) => `/app/enterprise_search${pathname}`),
push: jest.fn(),
location: {
pathname: '/current-path',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png
new file mode 100644
index 0000000000000..6cf0639167e2f
Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png differ
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png
new file mode 100644
index 0000000000000..1b5e1e489fd96
Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png differ
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png
new file mode 100644
index 0000000000000..984662b65cb5d
Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png differ
diff --git a/x-pack/plugins/infra/server/lib/snapshot/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts
similarity index 65%
rename from x-pack/plugins/infra/server/lib/snapshot/constants.ts
rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts
index 0420878dbcf50..df85a10f7e9de 100644
--- a/x-pack/plugins/infra/server/lib/snapshot/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts
@@ -4,6 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// TODO: Make SNAPSHOT_COMPOSITE_REQUEST_SIZE configurable from kibana.yml
-
-export const SNAPSHOT_COMPOSITE_REQUEST_SIZE = 75;
+export { ProductCard } from './product_card';
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss
new file mode 100644
index 0000000000000..d6b6bd3442590
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+.productCard {
+ margin: $euiSizeS;
+
+ &__imageContainer {
+ max-height: 115px;
+ overflow: hidden;
+ background-color: #0076cc;
+
+ @include euiBreakpoint('s', 'm', 'l', 'xl') {
+ max-height: none;
+ }
+ }
+
+ &__image {
+ width: 100%;
+ height: auto;
+ }
+
+ .euiCard__content {
+ max-width: 350px;
+ margin-top: $euiSizeL;
+
+ @include euiBreakpoint('s', 'm', 'l', 'xl') {
+ margin-top: $euiSizeXL;
+ }
+ }
+
+ .euiCard__title {
+ margin-bottom: $euiSizeM;
+ font-weight: $euiFontWeightBold;
+
+ @include euiBreakpoint('s', 'm', 'l', 'xl') {
+ margin-bottom: $euiSizeL;
+ font-size: $euiSizeL;
+ }
+ }
+
+ .euiCard__description {
+ font-weight: $euiFontWeightMedium;
+ color: $euiColorMediumShade;
+ margin-bottom: $euiSize;
+ }
+
+ .euiCard__footer {
+ margin-bottom: $euiSizeS;
+
+ @include euiBreakpoint('s', 'm', 'l', 'xl') {
+ margin-bottom: $euiSizeM;
+ font-size: $euiSizeL;
+ }
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx
new file mode 100644
index 0000000000000..a76b654ccddd0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { EuiCard } from '@elastic/eui';
+import { EuiButton } from '../../../shared/react_router_helpers';
+import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants';
+
+jest.mock('../../../shared/telemetry', () => ({
+ sendTelemetry: jest.fn(),
+}));
+import { sendTelemetry } from '../../../shared/telemetry';
+
+import { ProductCard } from './';
+
+describe('ProductCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders an App Search card', () => {
+ const wrapper = shallow();
+ const card = wrapper.find(EuiCard).dive().shallow();
+
+ expect(card.find('h2').text()).toEqual('Elastic App Search');
+ expect(card.find('.productCard__image').prop('src')).toEqual('as.jpg');
+
+ const button = card.find(EuiButton);
+ expect(button.prop('to')).toEqual('/app/enterprise_search/app_search');
+ expect(button.prop('data-test-subj')).toEqual('LaunchAppSearchButton');
+
+ button.simulate('click');
+ expect(sendTelemetry).toHaveBeenCalledWith(expect.objectContaining({ metric: 'app_search' }));
+ });
+
+ it('renders a Workplace Search card', () => {
+ const wrapper = shallow();
+ const card = wrapper.find(EuiCard).dive().shallow();
+
+ expect(card.find('h2').text()).toEqual('Elastic Workplace Search');
+ expect(card.find('.productCard__image').prop('src')).toEqual('ws.jpg');
+
+ const button = card.find(EuiButton);
+ expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search');
+ expect(button.prop('data-test-subj')).toEqual('LaunchWorkplaceSearchButton');
+
+ button.simulate('click');
+ expect(sendTelemetry).toHaveBeenCalledWith(
+ expect.objectContaining({ metric: 'workplace_search' })
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx
new file mode 100644
index 0000000000000..334ca126cabb9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import upperFirst from 'lodash/upperFirst';
+import snakeCase from 'lodash/snakeCase';
+import { i18n } from '@kbn/i18n';
+import { EuiCard, EuiTextColor } from '@elastic/eui';
+
+import { EuiButton } from '../../../shared/react_router_helpers';
+import { sendTelemetry } from '../../../shared/telemetry';
+import { KibanaContext, IKibanaContext } from '../../../index';
+
+import './product_card.scss';
+
+interface IProductCard {
+ // Expects product plugin constants (@see common/constants.ts)
+ product: {
+ ID: string;
+ NAME: string;
+ CARD_DESCRIPTION: string;
+ URL: string;
+ };
+ image: string;
+}
+
+export const ProductCard: React.FC = ({ product, image }) => {
+ const { http } = useContext(KibanaContext) as IKibanaContext;
+
+ return (
+
+
+
+ }
+ paddingSize="l"
+ description={{product.CARD_DESCRIPTION}}
+ footer={
+
+ sendTelemetry({
+ http,
+ product: 'enterprise_search',
+ action: 'clicked',
+ metric: snakeCase(product.ID),
+ })
+ }
+ data-test-subj={`Launch${upperFirst(product.ID)}Button`}
+ >
+ {i18n.translate('xpack.enterpriseSearch.overview.productCard.button', {
+ defaultMessage: `Launch {productName}`,
+ values: { productName: product.NAME },
+ })}
+
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss
new file mode 100644
index 0000000000000..d937943352317
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+.enterpriseSearchOverview {
+ padding-top: 78px;
+ background-image: url('./assets/bg_enterprise_search.png');
+ background-repeat: no-repeat;
+ background-size: 670px;
+ background-position: center -27px;
+
+ @include euiBreakpoint('m', 'l', 'xl') {
+ padding-top: 158px;
+ background-size: 1160px;
+ background-position: center -48px;
+ }
+
+ &__header {
+ text-align: center;
+ margin: auto;
+ }
+
+ &__heading {
+ @include euiBreakpoint('xs', 's') {
+ font-size: $euiFontSizeXL;
+ line-height: map-get(map-get($euiTitles, 'm'), 'line-height');
+ }
+ }
+
+ &__subheading {
+ color: $euiColorMediumShade;
+ font-size: $euiFontSize;
+
+ @include euiBreakpoint('m', 'l', 'xl') {
+ font-size: $euiFontSizeL;
+ margin-bottom: $euiSizeL;
+ }
+ }
+
+ // EUI override
+ .euiTitle + .euiTitle {
+ margin-top: 0;
+
+ @include euiBreakpoint('m', 'l', 'xl') {
+ margin-top: $euiSizeS;
+ }
+ }
+
+ .enterpriseSearchOverview__card {
+ flex-basis: 50%;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
new file mode 100644
index 0000000000000..cd2a22a45bbb4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { EuiPage } from '@elastic/eui';
+
+import { EnterpriseSearch } from './';
+import { ProductCard } from './components/product_card';
+
+describe('EnterpriseSearch', () => {
+ it('renders the overview page and product cards', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true);
+ expect(wrapper.find(ProductCard)).toHaveLength(2);
+ });
+
+ describe('access checks', () => {
+ it('does not render the App Search card if the user does not have access to AS', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find(ProductCard)).toHaveLength(1);
+ expect(wrapper.find(ProductCard).prop('product').ID).toEqual('workplaceSearch');
+ });
+
+ it('does not render the Workplace Search card if the user does not have access to WS', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find(ProductCard)).toHaveLength(1);
+ expect(wrapper.find(ProductCard).prop('product').ID).toEqual('appSearch');
+ });
+
+ it('does not render any cards if the user does not have access', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(ProductCard)).toHaveLength(0);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
new file mode 100644
index 0000000000000..373f595a6a9ea
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiPageContentBody,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { IInitialAppData } from '../../../common/types';
+import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants';
+
+import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome';
+import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry';
+
+import { ProductCard } from './components/product_card';
+
+import AppSearchImage from './assets/app_search.png';
+import WorkplaceSearchImage from './assets/workplace_search.png';
+import './index.scss';
+
+export const EnterpriseSearch: React.FC = ({ access = {} }) => {
+ const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.overview.heading', {
+ defaultMessage: 'Welcome to Elastic Enterprise Search',
+ })}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.overview.subheading', {
+ defaultMessage: 'Select a product to get started',
+ })}
+
+
+
+
+
+
+ {hasAppSearchAccess && (
+
+
+
+ )}
+ {hasWorkplaceSearchAccess && (
+
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
index 9e86b239432a7..3c8b3a7218862 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
@@ -37,27 +37,37 @@ describe('useBreadcrumbs', () => {
expect(breadcrumb).toEqual([
{
text: 'Hello',
- href: '/enterprise_search/hello',
+ href: '/app/enterprise_search/hello',
onClick: expect.any(Function),
},
{
text: 'World',
- href: '/enterprise_search/world',
+ href: '/app/enterprise_search/world',
onClick: expect.any(Function),
},
]);
});
it('prevents default navigation and uses React Router history on click', () => {
- const breadcrumb = useBreadcrumbs([{ text: '', path: '/' }])[0] as any;
+ const breadcrumb = useBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
const event = { preventDefault: jest.fn() };
breadcrumb.onClick(event);
- expect(mockKibanaContext.navigateToUrl).toHaveBeenCalled();
+ expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test');
expect(mockHistory.createHref).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
});
+ it('does not call createHref if shouldNotCreateHref is passed', () => {
+ const breadcrumb = useBreadcrumbs([
+ { text: '', path: '/test', shouldNotCreateHref: true },
+ ])[0] as any;
+ breadcrumb.onClick({ preventDefault: () => null });
+
+ expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/test');
+ expect(mockHistory.createHref).not.toHaveBeenCalled();
+ });
+
it('does not prevent default browser behavior on new tab/window clicks', () => {
const breadcrumb = useBreadcrumbs([{ text: '', path: '/' }])[0] as any;
@@ -95,15 +105,17 @@ describe('useEnterpriseSearchBreadcrumbs', () => {
expect(useEnterpriseSearchBreadcrumbs(breadcrumbs)).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
{
text: 'Page 1',
- href: '/enterprise_search/page1',
+ href: '/app/enterprise_search/page1',
onClick: expect.any(Function),
},
{
text: 'Page 2',
- href: '/enterprise_search/page2',
+ href: '/app/enterprise_search/page2',
onClick: expect.any(Function),
},
]);
@@ -113,6 +125,8 @@ describe('useEnterpriseSearchBreadcrumbs', () => {
expect(useEnterpriseSearchBreadcrumbs()).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
]);
});
@@ -122,7 +136,7 @@ describe('useAppSearchBreadcrumbs', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHistory.createHref.mockImplementation(
- ({ pathname }: any) => `/enterprise_search/app_search${pathname}`
+ ({ pathname }: any) => `/app/enterprise_search/app_search${pathname}`
);
});
@@ -141,20 +155,22 @@ describe('useAppSearchBreadcrumbs', () => {
expect(useAppSearchBreadcrumbs(breadcrumbs)).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
{
text: 'App Search',
- href: '/enterprise_search/app_search/',
+ href: '/app/enterprise_search/app_search/',
onClick: expect.any(Function),
},
{
text: 'Page 1',
- href: '/enterprise_search/app_search/page1',
+ href: '/app/enterprise_search/app_search/page1',
onClick: expect.any(Function),
},
{
text: 'Page 2',
- href: '/enterprise_search/app_search/page2',
+ href: '/app/enterprise_search/app_search/page2',
onClick: expect.any(Function),
},
]);
@@ -164,10 +180,12 @@ describe('useAppSearchBreadcrumbs', () => {
expect(useAppSearchBreadcrumbs()).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
{
text: 'App Search',
- href: '/enterprise_search/app_search/',
+ href: '/app/enterprise_search/app_search/',
onClick: expect.any(Function),
},
]);
@@ -178,7 +196,7 @@ describe('useWorkplaceSearchBreadcrumbs', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHistory.createHref.mockImplementation(
- ({ pathname }: any) => `/enterprise_search/workplace_search${pathname}`
+ ({ pathname }: any) => `/app/enterprise_search/workplace_search${pathname}`
);
});
@@ -197,20 +215,22 @@ describe('useWorkplaceSearchBreadcrumbs', () => {
expect(useWorkplaceSearchBreadcrumbs(breadcrumbs)).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
{
text: 'Workplace Search',
- href: '/enterprise_search/workplace_search/',
+ href: '/app/enterprise_search/workplace_search/',
onClick: expect.any(Function),
},
{
text: 'Page 1',
- href: '/enterprise_search/workplace_search/page1',
+ href: '/app/enterprise_search/workplace_search/page1',
onClick: expect.any(Function),
},
{
text: 'Page 2',
- href: '/enterprise_search/workplace_search/page2',
+ href: '/app/enterprise_search/workplace_search/page2',
onClick: expect.any(Function),
},
]);
@@ -220,10 +240,12 @@ describe('useWorkplaceSearchBreadcrumbs', () => {
expect(useWorkplaceSearchBreadcrumbs()).toEqual([
{
text: 'Enterprise Search',
+ href: '/app/enterprise_search/overview',
+ onClick: expect.any(Function),
},
{
text: 'Workplace Search',
- href: '/enterprise_search/workplace_search/',
+ href: '/app/enterprise_search/workplace_search/',
onClick: expect.any(Function),
},
]);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
index 6eab936719d01..19714608e73e9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
@@ -26,6 +26,9 @@ import { letBrowserHandleEvent } from '../react_router_helpers';
interface IBreadcrumb {
text: string;
path?: string;
+ // Used to navigate outside of the React Router basename,
+ // i.e. if we need to go from App Search to Enterprise Search
+ shouldNotCreateHref?: boolean;
}
export type TBreadcrumbs = IBreadcrumb[];
@@ -33,11 +36,11 @@ export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => {
const history = useHistory();
const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext;
- return breadcrumbs.map(({ text, path }) => {
+ return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => {
const breadcrumb = { text } as EuiBreadcrumb;
if (path) {
- const href = history.createHref({ pathname: path }) as string;
+ const href = shouldNotCreateHref ? path : (history.createHref({ pathname: path }) as string);
breadcrumb.href = href;
breadcrumb.onClick = (event) => {
@@ -56,7 +59,14 @@ export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => {
*/
export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) =>
- useBreadcrumbs([{ text: ENTERPRISE_SEARCH_PLUGIN.NAME }, ...breadcrumbs]);
+ useBreadcrumbs([
+ {
+ text: ENTERPRISE_SEARCH_PLUGIN.NAME,
+ path: ENTERPRISE_SEARCH_PLUGIN.URL,
+ shouldNotCreateHref: true,
+ },
+ ...breadcrumbs,
+ ]);
export const useAppSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) =>
useEnterpriseSearchBreadcrumbs([{ text: APP_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs]);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts
index 706baefc00cc2..de5f72de79192 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts
@@ -20,7 +20,7 @@ export type TTitle = string[];
/**
* Given an array of page titles, return a final formatted document title
* @param pages - e.g., ['Curations', 'some Engine', 'App Search']
- * @returns - e.g., 'Curations | some Engine | App Search'
+ * @returns - e.g., 'Curations - some Engine - App Search'
*/
export const generateTitle = (pages: TTitle) => pages.join(' - ');
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts
index 4468d11ba94c9..02013a03c3395 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts
@@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { SetAppSearchChrome, SetWorkplaceSearchChrome } from './set_chrome';
+export {
+ SetEnterpriseSearchChrome,
+ SetAppSearchChrome,
+ SetWorkplaceSearchChrome,
+} from './set_chrome';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx
index bda816c9a5554..61a066bb92216 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx
@@ -12,18 +12,24 @@ import React from 'react';
import { mockKibanaContext, mountWithKibanaContext } from '../../__mocks__';
jest.mock('./generate_breadcrumbs', () => ({
+ useEnterpriseSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs),
useAppSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs),
useWorkplaceSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs),
}));
-import { useAppSearchBreadcrumbs, useWorkplaceSearchBreadcrumbs } from './generate_breadcrumbs';
+import {
+ useEnterpriseSearchBreadcrumbs,
+ useAppSearchBreadcrumbs,
+ useWorkplaceSearchBreadcrumbs,
+} from './generate_breadcrumbs';
jest.mock('./generate_title', () => ({
+ enterpriseSearchTitle: jest.fn((title: any) => title),
appSearchTitle: jest.fn((title: any) => title),
workplaceSearchTitle: jest.fn((title: any) => title),
}));
-import { appSearchTitle, workplaceSearchTitle } from './generate_title';
+import { enterpriseSearchTitle, appSearchTitle, workplaceSearchTitle } from './generate_title';
-import { SetAppSearchChrome, SetWorkplaceSearchChrome } from './';
+import { SetEnterpriseSearchChrome, SetAppSearchChrome, SetWorkplaceSearchChrome } from './';
describe('Set Kibana Chrome helpers', () => {
beforeEach(() => {
@@ -35,6 +41,27 @@ describe('Set Kibana Chrome helpers', () => {
expect(mockKibanaContext.setDocTitle).toHaveBeenCalled();
});
+ describe('SetEnterpriseSearchChrome', () => {
+ it('sets breadcrumbs and document title', () => {
+ mountWithKibanaContext();
+
+ expect(enterpriseSearchTitle).toHaveBeenCalledWith(['Hello World']);
+ expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([
+ {
+ text: 'Hello World',
+ path: '/current-path',
+ },
+ ]);
+ });
+
+ it('sets empty breadcrumbs and document title when isRoot is true', () => {
+ mountWithKibanaContext();
+
+ expect(enterpriseSearchTitle).toHaveBeenCalledWith([]);
+ expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([]);
+ });
+ });
+
describe('SetAppSearchChrome', () => {
it('sets breadcrumbs and document title', () => {
mountWithKibanaContext();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx
index 43db93c1583d1..5e8d972e1a135 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx
@@ -10,11 +10,17 @@ import { EuiBreadcrumb } from '@elastic/eui';
import { KibanaContext, IKibanaContext } from '../../index';
import {
+ useEnterpriseSearchBreadcrumbs,
useAppSearchBreadcrumbs,
useWorkplaceSearchBreadcrumbs,
TBreadcrumbs,
} from './generate_breadcrumbs';
-import { appSearchTitle, workplaceSearchTitle, TTitle } from './generate_title';
+import {
+ enterpriseSearchTitle,
+ appSearchTitle,
+ workplaceSearchTitle,
+ TTitle,
+} from './generate_title';
/**
* Helpers for setting Kibana chrome (breadcrumbs, doc titles) on React view mount
@@ -33,6 +39,24 @@ interface IRootBreadcrumbsProps {
}
type TBreadcrumbsProps = IBreadcrumbsProps | IRootBreadcrumbsProps;
+export const SetEnterpriseSearchChrome: React.FC = ({ text, isRoot }) => {
+ const history = useHistory();
+ const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext;
+
+ const title = isRoot ? [] : [text];
+ const docTitle = enterpriseSearchTitle(title as TTitle | []);
+
+ const crumb = isRoot ? [] : [{ text, path: history.location.pathname }];
+ const breadcrumbs = useEnterpriseSearchBreadcrumbs(crumb as TBreadcrumbs | []);
+
+ useEffect(() => {
+ setBreadcrumbs(breadcrumbs);
+ setDocTitle(docTitle);
+ }, []);
+
+ return null;
+};
+
export const SetAppSearchChrome: React.FC = ({ text, isRoot }) => {
const history = useHistory();
const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
index 063118f94cd19..0c7bac99085dd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
@@ -45,10 +45,18 @@ describe('EUI & React Router Component Helpers', () => {
const link = wrapper.find(EuiLink);
expect(link.prop('onClick')).toBeInstanceOf(Function);
- expect(link.prop('href')).toEqual('/enterprise_search/foo/bar');
+ expect(link.prop('href')).toEqual('/app/enterprise_search/foo/bar');
expect(mockHistory.createHref).toHaveBeenCalled();
});
+ it('renders with the correct non-basenamed href when shouldNotCreateHref is passed', () => {
+ const wrapper = mount();
+ const link = wrapper.find(EuiLink);
+
+ expect(link.prop('href')).toEqual('/foo/bar');
+ expect(mockHistory.createHref).not.toHaveBeenCalled();
+ });
+
describe('onClick', () => {
it('prevents default navigation and uses React Router history', () => {
const wrapper = mount();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
index 7221a61d0997b..e3b46632ddf9e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
@@ -21,14 +21,22 @@ import { letBrowserHandleEvent } from './link_events';
interface IEuiReactRouterProps {
to: string;
onClick?(): void;
+ // Used to navigate outside of the React Router plugin basename but still within Kibana,
+ // e.g. if we need to go from Enterprise Search to App Search
+ shouldNotCreateHref?: boolean;
}
-export const EuiReactRouterHelper: React.FC = ({ to, onClick, children }) => {
+export const EuiReactRouterHelper: React.FC = ({
+ to,
+ onClick,
+ shouldNotCreateHref,
+ children,
+}) => {
const history = useHistory();
const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext;
// Generate the correct link href (with basename etc. accounted for)
- const href = history.createHref({ pathname: to });
+ const href = shouldNotCreateHref ? to : history.createHref({ pathname: to });
const reactRouterLinkClick = (event: React.MouseEvent) => {
if (onClick) onClick(); // Run any passed click events (e.g. telemetry)
@@ -51,9 +59,10 @@ type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps;
export const EuiReactRouterLink: React.FC = ({
to,
onClick,
+ shouldNotCreateHref,
...rest
}) => (
-
+
);
@@ -61,9 +70,10 @@ export const EuiReactRouterLink: React.FC = ({
export const EuiReactRouterButton: React.FC = ({
to,
onClick,
+ shouldNotCreateHref,
...rest
}) => (
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
index eadf7fa805590..a8b9636c3ff3e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
@@ -5,5 +5,8 @@
*/
export { sendTelemetry } from './send_telemetry';
-export { SendAppSearchTelemetry } from './send_telemetry';
-export { SendWorkplaceSearchTelemetry } from './send_telemetry';
+export {
+ SendEnterpriseSearchTelemetry,
+ SendAppSearchTelemetry,
+ SendWorkplaceSearchTelemetry,
+} from './send_telemetry';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
index 3c873dbc25e37..8f7cf090e2d57 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
@@ -10,7 +10,12 @@ import { httpServiceMock } from 'src/core/public/mocks';
import { JSON_HEADER as headers } from '../../../../common/constants';
import { mountWithKibanaContext } from '../../__mocks__';
-import { sendTelemetry, SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from './';
+import {
+ sendTelemetry,
+ SendEnterpriseSearchTelemetry,
+ SendAppSearchTelemetry,
+ SendWorkplaceSearchTelemetry,
+} from './';
describe('Shared Telemetry Helpers', () => {
const httpMock = httpServiceMock.createSetupContract();
@@ -44,6 +49,17 @@ describe('Shared Telemetry Helpers', () => {
});
describe('React component helpers', () => {
+ it('SendEnterpriseSearchTelemetry component', () => {
+ mountWithKibanaContext(, {
+ http: httpMock,
+ });
+
+ expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
+ headers,
+ body: '{"product":"enterprise_search","action":"viewed","metric":"page"}',
+ });
+ });
+
it('SendAppSearchTelemetry component', () => {
mountWithKibanaContext(, {
http: httpMock,
@@ -56,13 +72,13 @@ describe('Shared Telemetry Helpers', () => {
});
it('SendWorkplaceSearchTelemetry component', () => {
- mountWithKibanaContext(, {
+ mountWithKibanaContext(, {
http: httpMock,
});
expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
headers,
- body: '{"product":"workplace_search","action":"viewed","metric":"page"}',
+ body: '{"product":"workplace_search","action":"error","metric":"not_found"}',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
index 715d61b31512c..4df1428221de6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
@@ -35,9 +35,21 @@ export const sendTelemetry = async ({ http, product, action, metric }: ISendTele
/**
* React component helpers - useful for on-page-load/views
- * TODO: SendEnterpriseSearchTelemetry
*/
+export const SendEnterpriseSearchTelemetry: React.FC = ({
+ action,
+ metric,
+}) => {
+ const { http } = useContext(KibanaContext) as IKibanaContext;
+
+ useEffect(() => {
+ sendTelemetry({ http, action, metric, product: 'enterprise_search' });
+ }, [action, metric, http]);
+
+ return null;
+};
+
export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => {
const { http } = useContext(KibanaContext) as IKibanaContext;
diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts
index 83598a0dc971d..b735db7c49520 100644
--- a/x-pack/plugins/enterprise_search/public/plugin.ts
+++ b/x-pack/plugins/enterprise_search/public/plugin.ts
@@ -12,7 +12,6 @@ import {
AppMountParameters,
HttpSetup,
} from 'src/core/public';
-import { i18n } from '@kbn/i18n';
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
@@ -52,6 +51,25 @@ export class EnterpriseSearchPlugin implements Plugin {
}
public setup(core: CoreSetup, plugins: PluginsSetup) {
+ core.application.register({
+ id: ENTERPRISE_SEARCH_PLUGIN.ID,
+ title: ENTERPRISE_SEARCH_PLUGIN.NAV_TITLE,
+ appRoute: ENTERPRISE_SEARCH_PLUGIN.URL,
+ category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
+ mount: async (params: AppMountParameters) => {
+ const [coreStart] = await core.getStartServices();
+ const { chrome } = coreStart;
+ chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME);
+
+ await this.getInitialData(coreStart.http);
+
+ const { renderApp } = await import('./applications');
+ const { EnterpriseSearch } = await import('./applications/enterprise_search');
+
+ return renderApp(EnterpriseSearch, params, coreStart, plugins, this.config, this.data);
+ },
+ });
+
core.application.register({
id: APP_SEARCH_PLUGIN.ID,
title: APP_SEARCH_PLUGIN.NAME,
@@ -94,22 +112,10 @@ export class EnterpriseSearchPlugin implements Plugin {
plugins.home.featureCatalogue.registerSolution({
id: ENTERPRISE_SEARCH_PLUGIN.ID,
title: ENTERPRISE_SEARCH_PLUGIN.NAME,
- subtitle: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', {
- defaultMessage: 'Search everything',
- }),
+ subtitle: ENTERPRISE_SEARCH_PLUGIN.SUBTITLE,
icon: 'logoEnterpriseSearch',
- descriptions: [
- i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', {
- defaultMessage: 'Build a powerful search experience.',
- }),
- i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', {
- defaultMessage: 'Connect your users to relevant data.',
- }),
- i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', {
- defaultMessage: 'Unify your team content.',
- }),
- ],
- path: APP_SEARCH_PLUGIN.URL, // TODO: Change this to enterprise search overview page once available
+ descriptions: ENTERPRISE_SEARCH_PLUGIN.DESCRIPTIONS,
+ path: ENTERPRISE_SEARCH_PLUGIN.URL,
});
plugins.home.featureCatalogue.register({
diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts
new file mode 100644
index 0000000000000..c3e2aff6551c9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mockLogger } from '../../__mocks__';
+
+import { registerTelemetryUsageCollector } from './telemetry';
+
+describe('Enterprise Search Telemetry Usage Collector', () => {
+ const makeUsageCollectorStub = jest.fn();
+ const registerStub = jest.fn();
+ const usageCollectionMock = {
+ makeUsageCollector: makeUsageCollectorStub,
+ registerCollector: registerStub,
+ } as any;
+
+ const savedObjectsRepoStub = {
+ get: () => ({
+ attributes: {
+ 'ui_viewed.overview': 10,
+ 'ui_clicked.app_search': 2,
+ 'ui_clicked.workplace_search': 3,
+ },
+ }),
+ incrementCounter: jest.fn(),
+ };
+ const savedObjectsMock = {
+ createInternalRepository: jest.fn(() => savedObjectsRepoStub),
+ } as any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('registerTelemetryUsageCollector', () => {
+ it('should make and register the usage collector', () => {
+ registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger);
+
+ expect(registerStub).toHaveBeenCalledTimes(1);
+ expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
+ expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('enterprise_search');
+ expect(makeUsageCollectorStub.mock.calls[0][0].isReady()).toBe(true);
+ });
+ });
+
+ describe('fetchTelemetryMetrics', () => {
+ it('should return existing saved objects data', async () => {
+ registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger);
+ const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(savedObjectsCounts).toEqual({
+ ui_viewed: {
+ overview: 10,
+ },
+ ui_clicked: {
+ app_search: 2,
+ workplace_search: 3,
+ },
+ });
+ });
+
+ it('should return a default telemetry object if no saved data exists', async () => {
+ const emptySavedObjectsMock = {
+ createInternalRepository: () => ({
+ get: () => ({ attributes: null }),
+ }),
+ } as any;
+
+ registerTelemetryUsageCollector(usageCollectionMock, emptySavedObjectsMock, mockLogger);
+ const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(savedObjectsCounts).toEqual({
+ ui_viewed: {
+ overview: 0,
+ },
+ ui_clicked: {
+ app_search: 0,
+ workplace_search: 0,
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts
new file mode 100644
index 0000000000000..a124a185b9a34
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import { SavedObjectsServiceStart, Logger } from 'src/core/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+
+import { getSavedObjectAttributesFromRepo } from '../lib/telemetry';
+
+interface ITelemetry {
+ ui_viewed: {
+ overview: number;
+ };
+ ui_clicked: {
+ app_search: number;
+ workplace_search: number;
+ };
+}
+
+export const ES_TELEMETRY_NAME = 'enterprise_search_telemetry';
+
+/**
+ * Register the telemetry collector
+ */
+
+export const registerTelemetryUsageCollector = (
+ usageCollection: UsageCollectionSetup,
+ savedObjects: SavedObjectsServiceStart,
+ log: Logger
+) => {
+ const telemetryUsageCollector = usageCollection.makeUsageCollector({
+ type: 'enterprise_search',
+ fetch: async () => fetchTelemetryMetrics(savedObjects, log),
+ isReady: () => true,
+ schema: {
+ ui_viewed: {
+ overview: { type: 'long' },
+ },
+ ui_clicked: {
+ app_search: { type: 'long' },
+ workplace_search: { type: 'long' },
+ },
+ },
+ });
+ usageCollection.registerCollector(telemetryUsageCollector);
+};
+
+/**
+ * Fetch the aggregated telemetry metrics from our saved objects
+ */
+
+const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log: Logger) => {
+ const savedObjectsRepository = savedObjects.createInternalRepository();
+ const savedObjectAttributes = await getSavedObjectAttributesFromRepo(
+ ES_TELEMETRY_NAME,
+ savedObjectsRepository,
+ log
+ );
+
+ const defaultTelemetrySavedObject: ITelemetry = {
+ ui_viewed: {
+ overview: 0,
+ },
+ ui_clicked: {
+ app_search: 0,
+ workplace_search: 0,
+ },
+ };
+
+ // If we don't have an existing/saved telemetry object, return the default
+ if (!savedObjectAttributes) {
+ return defaultTelemetrySavedObject;
+ }
+
+ return {
+ ui_viewed: {
+ overview: get(savedObjectAttributes, 'ui_viewed.overview', 0),
+ },
+ ui_clicked: {
+ app_search: get(savedObjectAttributes, 'ui_clicked.app_search', 0),
+ workplace_search: get(savedObjectAttributes, 'ui_clicked.workplace_search', 0),
+ },
+ } as ITelemetry;
+};
diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts
index aae162c23ccb4..6cf0be9fd1f31 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts
@@ -15,7 +15,7 @@ import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { getSavedObjectAttributesFromRepo, incrementUICounter } from './telemetry';
-describe('App Search Telemetry Usage Collector', () => {
+describe('Telemetry helpers', () => {
beforeEach(() => {
jest.clearAllMocks();
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
index 323f79e63bc6f..8e3ae2cfbeb86 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
@@ -38,51 +38,63 @@ describe('callEnterpriseSearchConfigAPI', () => {
external_url: 'http://some.vanity.url/',
read_only_mode: false,
ilm_enabled: true,
+ is_federated_auth: false,
configured_limits: {
- max_document_byte_size: 102400,
- max_engines_per_meta_engine: 15,
+ app_search: {
+ engine: {
+ document_size_in_bytes: 102400,
+ source_engines_per_meta_engine: 15,
+ },
+ },
+ workplace_search: {
+ custom_api_source: {
+ document_size_in_bytes: 102400,
+ total_fields: 64,
+ },
+ },
+ },
+ },
+ current_user: {
+ name: 'someuser',
+ access: {
+ app_search: true,
+ workplace_search: false,
},
app_search: {
- account_id: 'some-id-string',
- onboarding_complete: true,
+ account: {
+ id: 'some-id-string',
+ onboarding_complete: true,
+ },
+ role: {
+ id: 'account_id:somestring|user_oid:somestring',
+ role_type: 'owner',
+ ability: {
+ access_all_engines: true,
+ destroy: ['session'],
+ manage: ['account_credentials', 'account_engines'], // etc
+ edit: ['LocoMoco::Account'], // etc
+ view: ['Engine'], // etc
+ credential_types: ['admin', 'private', 'search'],
+ available_role_types: ['owner', 'admin'],
+ },
+ },
},
workplace_search: {
- can_create_invitations: true,
- is_federated_auth: false,
organization: {
name: 'ACME Donuts',
default_org_name: 'My Organization',
},
- fp_account: {
+ account: {
id: 'some-id-string',
groups: ['Default', 'Cats'],
is_admin: true,
can_create_personal_sources: true,
+ can_create_invitations: true,
is_curated: false,
viewed_onboarding_page: true,
},
},
},
- current_user: {
- name: 'someuser',
- access: {
- app_search: true,
- workplace_search: false,
- },
- app_search_role: {
- id: 'account_id:somestring|user_oid:somestring',
- role_type: 'owner',
- ability: {
- access_all_engines: true,
- destroy: ['session'],
- manage: ['account_credentials', 'account_engines'], // etc
- edit: ['LocoMoco::Account'], // etc
- view: ['Engine'], // etc
- credential_types: ['admin', 'private', 'search'],
- available_role_types: ['owner', 'admin'],
- },
- },
- },
};
beforeEach(() => {
@@ -91,7 +103,7 @@ describe('callEnterpriseSearchConfigAPI', () => {
it('calls the config API endpoint', async () => {
fetchMock.mockImplementationOnce((url: string) => {
- expect(url).toEqual('http://localhost:3002/api/ent/v1/internal/client_config');
+ expect(url).toEqual('http://localhost:3002/api/ent/v2/internal/client_config');
return Promise.resolve(new Response(JSON.stringify(mockResponse)));
});
@@ -116,9 +128,20 @@ describe('callEnterpriseSearchConfigAPI', () => {
publicUrl: undefined,
readOnlyMode: false,
ilmEnabled: false,
+ isFederatedAuth: false,
configuredLimits: {
- maxDocumentByteSize: undefined,
- maxEnginesPerMetaEngine: undefined,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize: undefined,
+ maxEnginesPerMetaEngine: undefined,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize: undefined,
+ totalFields: undefined,
+ },
+ },
},
appSearch: {
accountId: undefined,
@@ -138,17 +161,16 @@ describe('callEnterpriseSearchConfigAPI', () => {
},
},
workplaceSearch: {
- canCreateInvitations: false,
- isFederatedAuth: false,
organization: {
name: undefined,
defaultOrgName: undefined,
},
- fpAccount: {
+ account: {
id: undefined,
groups: [],
isAdmin: false,
canCreatePersonalSources: false,
+ canCreateInvitations: false,
isCurated: false,
viewedOnboardingPage: false,
},
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
index c9cbec15169d9..10a75e59cb249 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
@@ -29,7 +29,7 @@ interface IReturn extends IInitialAppData {
* useful various settings (e.g. product access, external URL)
* needed by the Kibana plugin at the setup stage
*/
-const ENDPOINT = '/api/ent/v1/internal/client_config';
+const ENDPOINT = '/api/ent/v2/internal/client_config';
export const callEnterpriseSearchConfigAPI = async ({
config,
@@ -67,44 +67,60 @@ export const callEnterpriseSearchConfigAPI = async ({
publicUrl: stripTrailingSlash(data?.settings?.external_url),
readOnlyMode: !!data?.settings?.read_only_mode,
ilmEnabled: !!data?.settings?.ilm_enabled,
+ isFederatedAuth: !!data?.settings?.is_federated_auth, // i.e., not standard auth
configuredLimits: {
- maxDocumentByteSize: data?.settings?.configured_limits?.max_document_byte_size,
- maxEnginesPerMetaEngine: data?.settings?.configured_limits?.max_engines_per_meta_engine,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize:
+ data?.settings?.configured_limits?.app_search?.engine?.document_size_in_bytes,
+ maxEnginesPerMetaEngine:
+ data?.settings?.configured_limits?.app_search?.engine?.source_engines_per_meta_engine,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize:
+ data?.settings?.configured_limits?.workplace_search?.custom_api_source
+ ?.document_size_in_bytes,
+ totalFields:
+ data?.settings?.configured_limits?.workplace_search?.custom_api_source?.total_fields,
+ },
+ },
},
appSearch: {
- accountId: data?.settings?.app_search?.account_id,
- onBoardingComplete: !!data?.settings?.app_search?.onboarding_complete,
+ accountId: data?.current_user?.app_search?.account?.id,
+ onBoardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete,
role: {
- id: data?.current_user?.app_search_role?.id,
- roleType: data?.current_user?.app_search_role?.role_type,
+ id: data?.current_user?.app_search?.role?.id,
+ roleType: data?.current_user?.app_search?.role?.role_type,
ability: {
- accessAllEngines: !!data?.current_user?.app_search_role?.ability?.access_all_engines,
- destroy: data?.current_user?.app_search_role?.ability?.destroy || [],
- manage: data?.current_user?.app_search_role?.ability?.manage || [],
- edit: data?.current_user?.app_search_role?.ability?.edit || [],
- view: data?.current_user?.app_search_role?.ability?.view || [],
- credentialTypes: data?.current_user?.app_search_role?.ability?.credential_types || [],
+ accessAllEngines: !!data?.current_user?.app_search?.role?.ability?.access_all_engines,
+ destroy: data?.current_user?.app_search?.role?.ability?.destroy || [],
+ manage: data?.current_user?.app_search?.role?.ability?.manage || [],
+ edit: data?.current_user?.app_search?.role?.ability?.edit || [],
+ view: data?.current_user?.app_search?.role?.ability?.view || [],
+ credentialTypes: data?.current_user?.app_search?.role?.ability?.credential_types || [],
availableRoleTypes:
- data?.current_user?.app_search_role?.ability?.available_role_types || [],
+ data?.current_user?.app_search?.role?.ability?.available_role_types || [],
},
},
},
workplaceSearch: {
- canCreateInvitations: !!data?.settings?.workplace_search?.can_create_invitations,
- isFederatedAuth: !!data?.settings?.workplace_search?.is_federated_auth,
organization: {
- name: data?.settings?.workplace_search?.organization?.name,
- defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name,
+ name: data?.current_user?.workplace_search?.organization?.name,
+ defaultOrgName: data?.current_user?.workplace_search?.organization?.default_org_name,
},
- fpAccount: {
- id: data?.settings?.workplace_search?.fp_account.id,
- groups: data?.settings?.workplace_search?.fp_account.groups || [],
- isAdmin: !!data?.settings?.workplace_search?.fp_account?.is_admin,
- canCreatePersonalSources: !!data?.settings?.workplace_search?.fp_account
+ account: {
+ id: data?.current_user?.workplace_search?.account?.id,
+ groups: data?.current_user?.workplace_search?.account?.groups || [],
+ isAdmin: !!data?.current_user?.workplace_search?.account?.is_admin,
+ canCreatePersonalSources: !!data?.current_user?.workplace_search?.account
?.can_create_personal_sources,
- isCurated: !!data?.settings?.workplace_search?.fp_account.is_curated,
- viewedOnboardingPage: !!data?.settings?.workplace_search?.fp_account
- .viewed_onboarding_page,
+ canCreateInvitations: !!data?.current_user?.workplace_search?.account
+ ?.can_create_invitations,
+ isCurated: !!data?.current_user?.workplace_search?.account?.is_curated,
+ viewedOnboardingPage: !!data?.current_user?.workplace_search?.account
+ ?.viewed_onboarding_page,
},
},
};
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
index 617210a544262..729a03d24065e 100644
--- a/x-pack/plugins/enterprise_search/server/plugin.ts
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -31,8 +31,10 @@ import {
IEnterpriseSearchRequestHandler,
} from './lib/enterprise_search_request_handler';
-import { registerConfigDataRoute } from './routes/enterprise_search/config_data';
+import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry';
+import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry';
import { registerTelemetryRoute } from './routes/enterprise_search/telemetry';
+import { registerConfigDataRoute } from './routes/enterprise_search/config_data';
import { appSearchTelemetryType } from './saved_objects/app_search/telemetry';
import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry';
@@ -81,8 +83,12 @@ export class EnterpriseSearchPlugin implements Plugin {
name: ENTERPRISE_SEARCH_PLUGIN.NAME,
order: 0,
icon: 'logoEnterpriseSearch',
- navLinkId: APP_SEARCH_PLUGIN.ID, // TODO - remove this once functional tests no longer rely on navLinkId
- app: ['kibana', APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID],
+ app: [
+ 'kibana',
+ ENTERPRISE_SEARCH_PLUGIN.ID,
+ APP_SEARCH_PLUGIN.ID,
+ WORKPLACE_SEARCH_PLUGIN.ID,
+ ],
catalogue: [ENTERPRISE_SEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID],
privileges: null,
});
@@ -94,14 +100,16 @@ export class EnterpriseSearchPlugin implements Plugin {
const dependencies = { config, security, request, log };
const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies);
+ const showEnterpriseSearchOverview = hasAppSearchAccess || hasWorkplaceSearchAccess;
return {
navLinks: {
+ enterpriseSearch: showEnterpriseSearchOverview,
appSearch: hasAppSearchAccess,
workplaceSearch: hasWorkplaceSearchAccess,
},
catalogue: {
- enterpriseSearch: hasAppSearchAccess || hasWorkplaceSearchAccess,
+ enterpriseSearch: showEnterpriseSearchOverview,
appSearch: hasAppSearchAccess,
workplaceSearch: hasWorkplaceSearchAccess,
},
@@ -123,6 +131,7 @@ export class EnterpriseSearchPlugin implements Plugin {
/**
* Bootstrap the routes, saved objects, and collector for telemetry
*/
+ savedObjects.registerType(enterpriseSearchTelemetryType);
savedObjects.registerType(appSearchTelemetryType);
savedObjects.registerType(workplaceSearchTelemetryType);
let savedObjectsStarted: SavedObjectsServiceStart;
@@ -131,6 +140,7 @@ export class EnterpriseSearchPlugin implements Plugin {
savedObjectsStarted = coreStart.savedObjects;
if (usageCollection) {
+ registerESTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger);
registerASTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger);
registerWSTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger);
}
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts
index 7ed1d7b17753c..bfc07c8b64ef5 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts
@@ -9,12 +9,13 @@ import { schema } from '@kbn/config-schema';
import { IRouteDependencies } from '../../plugin';
import { incrementUICounter } from '../../collectors/lib/telemetry';
+import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry';
import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry';
import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry';
const productToTelemetryMap = {
+ enterprise_search: ES_TELEMETRY_NAME,
app_search: AS_TELEMETRY_NAME,
workplace_search: WS_TELEMETRY_NAME,
- enterprise_search: 'TODO',
};
export function registerTelemetryRoute({
diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts
new file mode 100644
index 0000000000000..54044e67939da
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* istanbul ignore file */
+
+import { SavedObjectsType } from 'src/core/server';
+import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry';
+
+export const enterpriseSearchTelemetryType: SavedObjectsType = {
+ name: ES_TELEMETRY_NAME,
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ dynamic: false,
+ properties: {},
+ },
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
index a9f6d2ea03bdf..6882ddea4ad5d 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
@@ -24,7 +24,7 @@ export interface DataTypeDefinition {
export interface ParameterDefinition {
title?: string;
description?: JSX.Element | string;
- fieldConfig: FieldConfig;
+ fieldConfig: FieldConfig;
schema?: any;
props?: { [key: string]: ParameterDefinition };
documentation?: {
diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts
index 818009417fb1c..4c729d11ba8c1 100644
--- a/x-pack/plugins/infra/common/http_api/index.ts
+++ b/x-pack/plugins/infra/common/http_api/index.ts
@@ -10,3 +10,4 @@ export * from './log_entries';
export * from './metrics_explorer';
export * from './metrics_api';
export * from './log_alerts';
+export * from './snapshot_api';
diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts
index 7436566f039ca..41657fdce2153 100644
--- a/x-pack/plugins/infra/common/http_api/metrics_api.ts
+++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts
@@ -33,7 +33,6 @@ export const MetricsAPIRequestRT = rt.intersection([
afterKey: rt.union([rt.null, afterKeyObjectRT]),
limit: rt.union([rt.number, rt.null, rt.undefined]),
filters: rt.array(rt.object),
- forceInterval: rt.boolean,
dropLastBucket: rt.boolean,
alignDataToEnd: rt.boolean,
}),
@@ -59,7 +58,10 @@ export const MetricsAPIRowRT = rt.intersection([
rt.type({
timestamp: rt.number,
}),
- rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
+ rt.record(
+ rt.string,
+ rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)])
+ ),
]);
export const MetricsAPISeriesRT = rt.intersection([
diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
index c5776e0b0ced1..460b2bf9d802e 100644
--- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
+++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
@@ -89,7 +89,10 @@ export const metricsExplorerRowRT = rt.intersection([
rt.type({
timestamp: rt.number,
}),
- rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
+ rt.record(
+ rt.string,
+ rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)])
+ ),
]);
export const metricsExplorerSeriesRT = rt.intersection([
diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts
index 11cb57238f917..e1b8dfa4770ba 100644
--- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts
+++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts
@@ -6,7 +6,7 @@
import * as rt from 'io-ts';
import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types';
-import { metricsExplorerSeriesRT } from './metrics_explorer';
+import { MetricsAPISeriesRT } from './metrics_api';
export const SnapshotNodePathRT = rt.intersection([
rt.type({
@@ -22,7 +22,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({
value: rt.union([rt.number, rt.null]),
avg: rt.union([rt.number, rt.null]),
max: rt.union([rt.number, rt.null]),
- timeseries: metricsExplorerSeriesRT,
+ timeseries: MetricsAPISeriesRT,
});
const SnapshotNodeMetricRequiredRT = rt.type({
@@ -36,6 +36,7 @@ export const SnapshotNodeMetricRT = rt.intersection([
export const SnapshotNodeRT = rt.type({
metrics: rt.array(SnapshotNodeMetricRT),
path: rt.array(SnapshotNodePathRT),
+ name: rt.string,
});
export const SnapshotNodeResponseRT = rt.type({
diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts
index 570220bbc7aa5..851646ef1fa12 100644
--- a/x-pack/plugins/infra/common/inventory_models/types.ts
+++ b/x-pack/plugins/infra/common/inventory_models/types.ts
@@ -281,6 +281,10 @@ export const ESSumBucketAggRT = rt.type({
}),
});
+export const ESTopHitsAggRT = rt.type({
+ top_hits: rt.object,
+});
+
interface SnapshotTermsWithAggregation {
terms: { field: string };
aggregations: MetricsUIAggregation;
@@ -304,6 +308,7 @@ export const ESAggregationRT = rt.union([
ESSumBucketAggRT,
ESTermsWithAggregationRT,
ESCaridnalityAggRT,
+ ESTopHitsAggRT,
]);
export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
index d2c30a4f38ee9..e01ca3ab6e844 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
@@ -88,6 +88,7 @@ describe('ConditionalToolTip', () => {
mockedUseSnapshot.mockReturnValue({
nodes: [
{
+ name: 'host-01',
path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }],
metrics: [
{ name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 },
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
index fbb6aa933219a..49f4b56532936 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
@@ -7,6 +7,7 @@ import { calculateBoundsFromNodes } from './calculate_bounds_from_nodes';
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
const nodes: SnapshotNode[] = [
{
+ name: 'host-01',
path: [{ value: 'host-01', label: 'host-01' }],
metrics: [
{
@@ -18,6 +19,7 @@ const nodes: SnapshotNode[] = [
],
},
{
+ name: 'host-02',
path: [{ value: 'host-02', label: 'host-02' }],
metrics: [
{
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
index 2a9f8b911c124..f7d9f029f00df 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
@@ -9,6 +9,7 @@ import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
const nodes: SnapshotNode[] = [
{
+ name: 'host-01',
path: [{ value: 'host-01', label: 'host-01' }],
metrics: [
{
@@ -20,6 +21,7 @@ const nodes: SnapshotNode[] = [
],
},
{
+ name: 'host-02',
path: [{ value: 'host-02', label: 'host-02' }],
metrics: [
{
diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
index 939498305eb98..c5b667fb20538 100644
--- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
+++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
@@ -8,12 +8,12 @@
import { timeMilliseconds } from 'd3-time';
import * as runtimeTypes from 'io-ts';
-import { compact, first, get, has } from 'lodash';
+import { compact, first } from 'lodash';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity, constant } from 'fp-ts/lib/function';
import { RequestHandlerContext } from 'src/core/server';
-import { JsonObject, JsonValue } from '../../../../common/typed_json';
+import { JsonValue } from '../../../../common/typed_json';
import {
LogEntriesAdapter,
LogEntriesParams,
@@ -31,7 +31,7 @@ const TIMESTAMP_FORMAT = 'epoch_millis';
interface LogItemHit {
_index: string;
_id: string;
- _source: JsonObject;
+ fields: { [key: string]: [value: unknown] };
sort: [number, number];
}
@@ -82,7 +82,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
body: {
size: typeof size !== 'undefined' ? size : LOG_ENTRIES_PAGE_SIZE,
track_total_hits: false,
- _source: fields,
+ _source: false,
+ fields,
query: {
bool: {
filter: [
@@ -214,6 +215,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
values: [id],
},
},
+ fields: ['*'],
+ _source: false,
},
};
@@ -230,8 +233,8 @@ function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]):
return hits.map((hit) => {
const logFields = fields.reduce<{ [fieldName: string]: JsonValue }>(
(flattenedFields, field) => {
- if (has(hit._source, field)) {
- flattenedFields[field] = get(hit._source, field);
+ if (field in hit.fields) {
+ flattenedFields[field] = hit.fields[field][0];
}
return flattenedFields;
},
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index 2f3593a11f664..d6592719d0723 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -16,12 +16,11 @@ import {
} from '../../adapters/framework/adapter_types';
import { Comparator, InventoryMetricConditions } from './types';
import { AlertServices } from '../../../../../alerts/server';
-import { InfraSnapshot } from '../../snapshot';
-import { parseFilterQuery } from '../../../utils/serialized_query';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
-import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api';
-import { InfraSourceConfiguration } from '../../sources';
+import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_api/snapshot_api';
+import { InfraSource } from '../../sources';
import { UNGROUPED_FACTORY_KEY } from '../common/utils';
+import { getNodes } from '../../../routes/snapshot/lib/get_nodes';
type ConditionResult = InventoryMetricConditions & {
shouldFire: boolean[];
@@ -33,7 +32,7 @@ type ConditionResult = InventoryMetricConditions & {
export const evaluateCondition = async (
condition: InventoryMetricConditions,
nodeType: InventoryItemType,
- sourceConfiguration: InfraSourceConfiguration,
+ source: InfraSource,
callCluster: AlertServices['callCluster'],
filterQuery?: string,
lookbackSize?: number
@@ -55,7 +54,7 @@ export const evaluateCondition = async (
nodeType,
metric,
timerange,
- sourceConfiguration,
+ source,
filterQuery,
customMetric
);
@@ -94,12 +93,11 @@ const getData = async (
nodeType: InventoryItemType,
metric: SnapshotMetricType,
timerange: InfraTimerangeInput,
- sourceConfiguration: InfraSourceConfiguration,
+ source: InfraSource,
filterQuery?: string,
customMetric?: SnapshotCustomMetricInput
) => {
- const snapshot = new InfraSnapshot();
- const esClient = (
+ const client = (
options: CallWithRequestParams
): Promise> => callCluster('search', options);
@@ -107,17 +105,17 @@ const getData = async (
metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric },
];
- const options = {
- filterQuery: parseFilterQuery(filterQuery),
+ const snapshotRequest: SnapshotRequest = {
+ filterQuery,
nodeType,
groupBy: [],
- sourceConfiguration,
+ sourceId: 'default',
metrics,
timerange,
includeTimeseries: Boolean(timerange.lookbackSize),
};
try {
- const { nodes } = await snapshot.getNodes(esClient, options);
+ const { nodes } = await getNodes(client, snapshotRequest, source);
if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index bdac9dcd1dee8..99904f15b4606 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -50,9 +50,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
);
const results = await Promise.all(
- criteria.map((c) =>
- evaluateCondition(c, nodeType, source.configuration, services.callCluster, filterQuery)
- )
+ criteria.map((c) => evaluateCondition(c, nodeType, source, services.callCluster, filterQuery))
);
const inventoryItems = Object.keys(first(results)!);
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
index 755c395818f5a..2ab015b6b37a2 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
@@ -26,7 +26,7 @@ interface InventoryMetricThresholdParams {
interface PreviewInventoryMetricThresholdAlertParams {
callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
params: InventoryMetricThresholdParams;
- config: InfraSource['configuration'];
+ source: InfraSource;
lookback: Unit;
alertInterval: string;
}
@@ -34,7 +34,7 @@ interface PreviewInventoryMetricThresholdAlertParams {
export const previewInventoryMetricThresholdAlert = async ({
callCluster,
params,
- config,
+ source,
lookback,
alertInterval,
}: PreviewInventoryMetricThresholdAlertParams) => {
@@ -55,7 +55,7 @@ export const previewInventoryMetricThresholdAlert = async ({
try {
const results = await Promise.all(
criteria.map((c) =>
- evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize)
+ evaluateCondition(c, nodeType, source, callCluster, filterQuery, lookbackSize)
)
);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
index 078ca46d42e60..8696081043ff7 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
@@ -8,8 +8,8 @@ import { networkTraffic } from '../../../../../common/inventory_models/shared/me
import { MetricExpressionParams, Aggregators } from '../types';
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
import { roundTimestamp } from '../../../../utils/round_timestamp';
-import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
import { createPercentileAggregation } from './create_percentile_aggregation';
+import { calculateDateHistogramOffset } from '../../../metrics/lib/calculate_date_histogram_offset';
const MINIMUM_BUCKETS = 5;
@@ -46,7 +46,7 @@ export const getElasticsearchMetricQuery = (
timeUnit
);
- const offset = getDateHistogramOffset(from, interval);
+ const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield });
const aggregations =
aggType === Aggregators.COUNT
diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
index 099e7c3b5038c..7c8560d72ff97 100644
--- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
+++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
@@ -20,6 +20,11 @@ const serializeValue = (value: any): string => {
}
return `${value}`;
};
+export const convertESFieldsToLogItemFields = (fields: {
+ [field: string]: [value: unknown];
+}): LogEntriesItemField[] => {
+ return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field][0]) }));
+};
export const convertDocumentSourceToLogItemFields = (
source: JsonObject,
diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
index 9b3e31f4da87a..e211f72b4e076 100644
--- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
+++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
@@ -22,7 +22,7 @@ import {
SavedSourceConfigurationFieldColumnRuntimeType,
} from '../../sources';
import { getBuiltinRules } from './builtin_rules';
-import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
+import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields';
import {
CompiledLogMessageFormattingRule,
Fields,
@@ -264,7 +264,7 @@ export class InfraLogEntriesDomain {
tiebreaker: document.sort[1],
},
fields: sortBy(
- [...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)],
+ [...defaultFields, ...convertESFieldsToLogItemFields(document.fields)],
'field'
),
};
@@ -313,7 +313,7 @@ export class InfraLogEntriesDomain {
interface LogItemHit {
_index: string;
_id: string;
- _source: JsonObject;
+ fields: { [field: string]: [value: unknown] };
sort: [number, number];
}
diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts
index 9896ad6ac1cd1..084ece52302b0 100644
--- a/x-pack/plugins/infra/server/lib/infra_types.ts
+++ b/x-pack/plugins/infra/server/lib/infra_types.ts
@@ -8,7 +8,6 @@ import { InfraSourceConfiguration } from '../../common/graphql/types';
import { InfraFieldsDomain } from './domains/fields_domain';
import { InfraLogEntriesDomain } from './domains/log_entries_domain';
import { InfraMetricsDomain } from './domains/metrics_domain';
-import { InfraSnapshot } from './snapshot';
import { InfraSources } from './sources';
import { InfraSourceStatus } from './source_status';
import { InfraConfig } from '../plugin';
@@ -30,7 +29,6 @@ export interface InfraDomainLibs {
export interface InfraBackendLibs extends InfraDomainLibs {
configuration: InfraConfig;
framework: KibanaFramework;
- snapshot: InfraSnapshot;
sources: InfraSources;
sourceStatus: InfraSourceStatus;
}
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
index d2d90914eced5..2cbbc623aed38 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
@@ -53,7 +53,6 @@ Object {
"groupBy0": Object {
"terms": Object {
"field": "host.name",
- "order": "asc",
},
},
},
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
index 95e6ece215133..90e584368e9ad 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
@@ -5,6 +5,7 @@
*/
import { get, values, first } from 'lodash';
+import * as rt from 'io-ts';
import {
MetricsAPIRequest,
MetricsAPISeries,
@@ -13,15 +14,20 @@ import {
} from '../../../../common/http_api/metrics_api';
import {
HistogramBucket,
- MetricValueType,
BasicMetricValueRT,
NormalizedMetricValueRT,
PercentilesTypeRT,
PercentilesKeyedTypeRT,
+ TopHitsTypeRT,
+ MetricValueTypeRT,
} from '../types';
+
const BASE_COLUMNS = [{ name: 'timestamp', type: 'date' }] as MetricsAPIColumn[];
-const getValue = (valueObject: string | number | MetricValueType) => {
+const ValueObjectTypeRT = rt.union([rt.string, rt.number, MetricValueTypeRT]);
+type ValueObjectType = rt.TypeOf;
+
+const getValue = (valueObject: ValueObjectType) => {
if (NormalizedMetricValueRT.is(valueObject)) {
return valueObject.normalized_value || valueObject.value;
}
@@ -50,6 +56,10 @@ const getValue = (valueObject: string | number | MetricValueType) => {
return valueObject.value;
}
+ if (TopHitsTypeRT.is(valueObject)) {
+ return valueObject.hits.hits.map((hit) => hit._source);
+ }
+
return null;
};
@@ -61,8 +71,8 @@ const convertBucketsToRows = (
const ids = options.metrics.map((metric) => metric.id);
const metrics = ids.reduce((acc, id) => {
const valueObject = get(bucket, [id]);
- return { ...acc, [id]: getValue(valueObject) };
- }, {} as Record);
+ return { ...acc, [id]: ValueObjectTypeRT.is(valueObject) ? getValue(valueObject) : null };
+ }, {} as Record);
return { timestamp: bucket.key as number, ...metrics };
});
};
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
index 991e5febfc634..63fdbb3d2b30f 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
@@ -33,7 +33,7 @@ export const createAggregations = (options: MetricsAPIRequest) => {
composite: {
size: limit,
sources: options.groupBy.map((field, index) => ({
- [`groupBy${index}`]: { terms: { field, order: 'asc' } },
+ [`groupBy${index}`]: { terms: { field } },
})),
},
aggs: histogramAggregation,
diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts
index d1866470e0cf9..8746614b559d6 100644
--- a/x-pack/plugins/infra/server/lib/metrics/types.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/types.ts
@@ -25,17 +25,51 @@ export const PercentilesKeyedTypeRT = rt.type({
values: rt.array(rt.type({ key: rt.string, value: NumberOrNullRT })),
});
+export const TopHitsTypeRT = rt.type({
+ hits: rt.type({
+ total: rt.type({
+ value: rt.number,
+ relation: rt.string,
+ }),
+ hits: rt.array(
+ rt.intersection([
+ rt.type({
+ _index: rt.string,
+ _id: rt.string,
+ _score: NumberOrNullRT,
+ _source: rt.object,
+ }),
+ rt.partial({
+ sort: rt.array(rt.union([rt.string, rt.number])),
+ max_score: NumberOrNullRT,
+ }),
+ ])
+ ),
+ }),
+});
+
export const MetricValueTypeRT = rt.union([
BasicMetricValueRT,
NormalizedMetricValueRT,
PercentilesTypeRT,
PercentilesKeyedTypeRT,
+ TopHitsTypeRT,
]);
export type MetricValueType = rt.TypeOf;
+export const TermsWithMetrics = rt.intersection([
+ rt.type({
+ buckets: rt.array(rt.record(rt.string, rt.union([rt.number, rt.string, MetricValueTypeRT]))),
+ }),
+ rt.partial({
+ sum_other_doc_count: rt.number,
+ doc_count_error_upper_bound: rt.number,
+ }),
+]);
+
export const HistogramBucketRT = rt.record(
rt.string,
- rt.union([rt.number, rt.string, MetricValueTypeRT])
+ rt.union([rt.number, rt.string, MetricValueTypeRT, TermsWithMetrics])
);
export const HistogramResponseRT = rt.type({
diff --git a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts
deleted file mode 100644
index ca63043ba868e..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index';
-import { InfraSnapshotRequestOptions } from './types';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
-import {
- MetricsUIAggregation,
- MetricsUIAggregationRT,
- InventoryItemType,
-} from '../../../common/inventory_models/types';
-import {
- SnapshotMetricInput,
- SnapshotCustomMetricInputRT,
-} from '../../../common/http_api/snapshot_api';
-import { networkTraffic } from '../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
-
-interface GroupBySource {
- [id: string]: {
- terms: {
- field: string | null | undefined;
- missing_bucket?: boolean;
- };
- };
-}
-
-export const getFieldByNodeType = (options: InfraSnapshotRequestOptions) => {
- const inventoryFields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- return inventoryFields.id;
-};
-
-export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) => {
- const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- const sources: GroupBySource[] = options.groupBy.map((gb) => {
- return { [`${gb.field}`]: { terms: { field: gb.field } } };
- });
- sources.push({
- id: {
- terms: { field: fields.id },
- },
- });
- sources.push({
- name: { terms: { field: fields.name, missing_bucket: true } },
- });
- return sources;
-};
-
-export const getMetricsSources = (options: InfraSnapshotRequestOptions) => {
- const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- return [{ id: { terms: { field: fields.id } } }];
-};
-
-export const metricToAggregation = (
- nodeType: InventoryItemType,
- metric: SnapshotMetricInput,
- index: number
-) => {
- const inventoryModel = findInventoryModel(nodeType);
- if (SnapshotCustomMetricInputRT.is(metric)) {
- if (metric.aggregation === 'rate') {
- return networkTraffic(`custom_${index}`, metric.field);
- }
- return {
- [`custom_${index}`]: {
- [metric.aggregation]: {
- field: metric.field,
- },
- },
- };
- }
- return inventoryModel.metrics.snapshot?.[metric.type];
-};
-
-export const getMetricsAggregations = (
- options: InfraSnapshotRequestOptions
-): MetricsUIAggregation => {
- const { metrics } = options;
- return metrics.reduce((aggs, metric, index) => {
- const aggregation = metricToAggregation(options.nodeType, metric, index);
- if (!MetricsUIAggregationRT.is(aggregation)) {
- throw new Error(
- i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
- defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
- values: {
- nodeType: options.nodeType,
- metric: metric.type,
- },
- })
- );
- }
- return { ...aggs, ...aggregation };
- }, {});
-};
-
-export const getDateHistogramOffset = (from: number, interval: string): string => {
- const fromInSeconds = Math.floor(from / 1000);
- const bucketSizeInSeconds = getIntervalInSeconds(interval);
-
- // negative offset to align buckets with full intervals (e.g. minutes)
- const offset = (fromInSeconds % bucketSizeInSeconds) - bucketSizeInSeconds;
- return `${offset}s`;
-};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts
deleted file mode 100644
index 74840afc157d2..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- isIPv4,
- getIPFromBucket,
- InfraSnapshotNodeGroupByBucket,
- getMetricValueFromBucket,
- InfraSnapshotMetricsBucket,
-} from './response_helpers';
-
-describe('InfraOps ResponseHelpers', () => {
- describe('isIPv4', () => {
- it('should return true for IPv4', () => {
- expect(isIPv4('192.168.2.4')).toBe(true);
- });
- it('should return false for anything else', () => {
- expect(isIPv4('0:0:0:0:0:0:0:1')).toBe(false);
- });
- });
-
- describe('getIPFromBucket', () => {
- it('should return IPv4 address', () => {
- const bucket: InfraSnapshotNodeGroupByBucket = {
- key: {
- id: 'example-01',
- name: 'example-01',
- },
- ip: {
- hits: {
- total: { value: 1 },
- hits: [
- {
- _index: 'metricbeat-2019-01-01',
- _type: '_doc',
- _id: '29392939',
- _score: null,
- sort: [],
- _source: {
- host: {
- ip: ['2001:db8:85a3::8a2e:370:7334', '192.168.1.4'],
- },
- },
- },
- ],
- },
- },
- };
- expect(getIPFromBucket('host', bucket)).toBe('192.168.1.4');
- });
- it('should NOT return ipv6 address', () => {
- const bucket: InfraSnapshotNodeGroupByBucket = {
- key: {
- id: 'example-01',
- name: 'example-01',
- },
- ip: {
- hits: {
- total: { value: 1 },
- hits: [
- {
- _index: 'metricbeat-2019-01-01',
- _type: '_doc',
- _id: '29392939',
- _score: null,
- sort: [],
- _source: {
- host: {
- ip: ['2001:db8:85a3::8a2e:370:7334'],
- },
- },
- },
- ],
- },
- },
- };
- expect(getIPFromBucket('host', bucket)).toBe(null);
- });
- });
-
- describe('getMetricValueFromBucket', () => {
- it('should return the value of a bucket with data', () => {
- expect(getMetricValueFromBucket('custom', testBucket, 1)).toBe(0.5);
- });
- it('should return the normalized value of a bucket with data', () => {
- expect(getMetricValueFromBucket('cpu', testNormalizedBucket, 1)).toBe(50);
- });
- it('should return null for a bucket with no data', () => {
- expect(getMetricValueFromBucket('custom', testEmptyBucket, 1)).toBe(null);
- });
- });
-});
-
-// Hack to get around TypeScript
-const buckets = [
- {
- key: 'a',
- doc_count: 1,
- custom_1: {
- value: 0.5,
- },
- },
- {
- key: 'b',
- doc_count: 1,
- cpu: {
- value: 0.5,
- normalized_value: 50,
- },
- },
- {
- key: 'c',
- doc_count: 0,
- },
-] as InfraSnapshotMetricsBucket[];
-const [testBucket, testNormalizedBucket, testEmptyBucket] = buckets;
diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts
deleted file mode 100644
index 2652e362b7eff..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { isNumber, last, max, sum, get } from 'lodash';
-import moment from 'moment';
-
-import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
-import { InfraSnapshotRequestOptions } from './types';
-import { findInventoryModel } from '../../../common/inventory_models';
-import { InventoryItemType, SnapshotMetricType } from '../../../common/inventory_models/types';
-import { SnapshotNodeMetric, SnapshotNodePath } from '../../../common/http_api/snapshot_api';
-
-export interface InfraSnapshotNodeMetricsBucket {
- key: { id: string };
- histogram: {
- buckets: InfraSnapshotMetricsBucket[];
- };
-}
-
-// Jumping through TypeScript hoops here:
-// We need an interface that has the known members 'key' and 'doc_count' and also
-// an unknown number of members with unknown names but known format, containing the
-// metrics.
-// This union type is the only way I found to express this that TypeScript accepts.
-export interface InfraSnapshotBucketWithKey {
- key: string | number;
- doc_count: number;
-}
-
-export interface InfraSnapshotBucketWithValues {
- [name: string]: { value: number; normalized_value?: number };
-}
-
-export type InfraSnapshotMetricsBucket = InfraSnapshotBucketWithKey & InfraSnapshotBucketWithValues;
-
-interface InfraSnapshotIpHit {
- _index: string;
- _type: string;
- _id: string;
- _score: number | null;
- _source: {
- host: {
- ip: string[] | string;
- };
- };
- sort: number[];
-}
-
-export interface InfraSnapshotNodeGroupByBucket {
- key: {
- id: string;
- name: string;
- [groupByField: string]: string;
- };
- ip: {
- hits: {
- total: { value: number };
- hits: InfraSnapshotIpHit[];
- };
- };
-}
-
-export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject);
-
-export const getIPFromBucket = (
- nodeType: InventoryItemType,
- bucket: InfraSnapshotNodeGroupByBucket
-): string | null => {
- const inventoryModel = findInventoryModel(nodeType);
- if (!inventoryModel.fields.ip) {
- return null;
- }
- const ip = get(bucket, `ip.hits.hits[0]._source.${inventoryModel.fields.ip}`, null) as
- | string[]
- | null;
- if (Array.isArray(ip)) {
- return ip.find(isIPv4) || null;
- } else if (typeof ip === 'string') {
- return ip;
- }
-
- return null;
-};
-
-export const getNodePath = (
- groupBucket: InfraSnapshotNodeGroupByBucket,
- options: InfraSnapshotRequestOptions
-): SnapshotNodePath[] => {
- const node = groupBucket.key;
- const path = options.groupBy.map((gb) => {
- return { value: node[`${gb.field}`], label: node[`${gb.field}`] } as SnapshotNodePath;
- });
- const ip = getIPFromBucket(options.nodeType, groupBucket);
- path.push({ value: node.id, label: node.name || node.id, ip });
- return path;
-};
-
-interface NodeMetricsForLookup {
- [nodeId: string]: InfraSnapshotMetricsBucket[];
-}
-
-export const getNodeMetricsForLookup = (
- metrics: InfraSnapshotNodeMetricsBucket[]
-): NodeMetricsForLookup => {
- return metrics.reduce((acc: NodeMetricsForLookup, metric) => {
- acc[`${metric.key.id}`] = metric.histogram.buckets;
- return acc;
- }, {});
-};
-
-// In the returned object,
-// value contains the value from the last bucket spanning a full interval
-// max and avg are calculated from all buckets returned for the timerange
-export const getNodeMetrics = (
- nodeBuckets: InfraSnapshotMetricsBucket[],
- options: InfraSnapshotRequestOptions
-): SnapshotNodeMetric[] => {
- if (!nodeBuckets) {
- return options.metrics.map((metric) => ({
- name: metric.type,
- value: null,
- max: null,
- avg: null,
- }));
- }
- const lastBucket = findLastFullBucket(nodeBuckets, options);
- if (!lastBucket) return [];
- return options.metrics.map((metric, index) => {
- const metricResult: SnapshotNodeMetric = {
- name: metric.type,
- value: getMetricValueFromBucket(metric.type, lastBucket, index),
- max: calculateMax(nodeBuckets, metric.type, index),
- avg: calculateAvg(nodeBuckets, metric.type, index),
- };
- if (options.includeTimeseries) {
- metricResult.timeseries = getTimeseriesData(nodeBuckets, metric.type, index);
- }
- return metricResult;
- });
-};
-
-const findLastFullBucket = (
- buckets: InfraSnapshotMetricsBucket[],
- options: InfraSnapshotRequestOptions
-) => {
- const to = moment.utc(options.timerange.to);
- const bucketSize = getIntervalInSeconds(options.timerange.interval);
- return buckets.reduce((current, item) => {
- const itemKey = isNumber(item.key) ? item.key : parseInt(item.key, 10);
- const date = moment.utc(itemKey + bucketSize * 1000);
- if (!date.isAfter(to) && item.doc_count > 0) {
- return item;
- }
- return current;
- }, last(buckets));
-};
-
-export const getMetricValueFromBucket = (
- type: SnapshotMetricType,
- bucket: InfraSnapshotMetricsBucket,
- index: number
-) => {
- const key = type === 'custom' ? `custom_${index}` : type;
- const metric = bucket[key];
- const value = metric && (metric.normalized_value || metric.value);
- return isFinite(value) ? value : null;
-};
-
-function calculateMax(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-) {
- return max(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) || 0;
-}
-
-function calculateAvg(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-) {
- return (
- sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) / buckets.length ||
- 0
- );
-}
-
-function getTimeseriesData(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-): MetricsExplorerSeries {
- return {
- id: type,
- columns: [
- { name: 'timestamp', type: 'date' },
- { name: 'metric_0', type: 'number' },
- ],
- rows: buckets.map((bucket) => ({
- timestamp: bucket.key as number,
- metric_0: getMetricValueFromBucket(type, bucket, index),
- })),
- };
-}
diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts
deleted file mode 100644
index 33d8e738a717e..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework';
-
-import { JsonObject } from '../../../common/typed_json';
-import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants';
-import {
- getGroupedNodesSources,
- getMetricsAggregations,
- getMetricsSources,
- getDateHistogramOffset,
-} from './query_helpers';
-import {
- getNodeMetrics,
- getNodeMetricsForLookup,
- getNodePath,
- InfraSnapshotNodeGroupByBucket,
- InfraSnapshotNodeMetricsBucket,
-} from './response_helpers';
-import { getAllCompositeData } from '../../utils/get_all_composite_data';
-import { createAfterKeyHandler } from '../../utils/create_afterkey_handler';
-import { findInventoryModel } from '../../../common/inventory_models';
-import { InfraSnapshotRequestOptions } from './types';
-import { createTimeRangeWithInterval } from './create_timerange_with_interval';
-import { SnapshotNode } from '../../../common/http_api/snapshot_api';
-
-type NamedSnapshotNode = SnapshotNode & { name: string };
-
-export type ESSearchClient = (
- options: CallWithRequestParams
-) => Promise>;
-export class InfraSnapshot {
- public async getNodes(
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
- ): Promise<{ nodes: NamedSnapshotNode[]; interval: string }> {
- // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch
- // in order to page through the results of their respective composite aggregations.
- // Both chains of requests are supposed to run in parallel, and their results be merged
- // when they have both been completed.
- const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options);
- const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied };
-
- const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange);
- const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange);
- const [groupedNodeBuckets, nodeMetricBuckets] = await Promise.all([
- groupedNodesPromise,
- nodeMetricsPromise,
- ]);
- return {
- nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options),
- interval: timeRangeWithIntervalApplied.interval,
- };
- }
-}
-
-const bucketSelector = (
- response: InfraDatabaseSearchResponse<{}, InfraSnapshotAggregationResponse>
-) => (response.aggregations && response.aggregations.nodes.buckets) || [];
-
-const handleAfterKey = createAfterKeyHandler(
- 'body.aggregations.nodes.composite.after',
- (input) => input?.aggregations?.nodes?.after_key
-);
-
-const callClusterFactory = (search: ESSearchClient) => (opts: any) =>
- search<{}, InfraSnapshotAggregationResponse>(opts);
-
-const requestGroupedNodes = async (
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
-): Promise => {
- const inventoryModel = findInventoryModel(options.nodeType);
- const query = {
- allowNoIndices: true,
- index: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`,
- ignoreUnavailable: true,
- body: {
- query: {
- bool: {
- filter: buildFilters(options),
- },
- },
- size: 0,
- aggregations: {
- nodes: {
- composite: {
- size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE,
- sources: getGroupedNodesSources(options),
- },
- aggs: {
- ip: {
- top_hits: {
- sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }],
- _source: {
- includes: inventoryModel.fields.ip ? [inventoryModel.fields.ip] : [],
- },
- size: 1,
- },
- },
- },
- },
- },
- },
- };
- return getAllCompositeData(
- callClusterFactory(client),
- query,
- bucketSelector,
- handleAfterKey
- );
-};
-
-const calculateIndexPatterBasedOnMetrics = (options: InfraSnapshotRequestOptions) => {
- const { metrics } = options;
- if (metrics.every((m) => m.type === 'logRate')) {
- return options.sourceConfiguration.logAlias;
- }
- if (metrics.some((m) => m.type === 'logRate')) {
- return `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`;
- }
- return options.sourceConfiguration.metricAlias;
-};
-
-const requestNodeMetrics = async (
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
-): Promise => {
- const index = calculateIndexPatterBasedOnMetrics(options);
- const query = {
- allowNoIndices: true,
- index,
- ignoreUnavailable: true,
- body: {
- query: {
- bool: {
- filter: buildFilters(options, false),
- },
- },
- size: 0,
- aggregations: {
- nodes: {
- composite: {
- size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE,
- sources: getMetricsSources(options),
- },
- aggregations: {
- histogram: {
- date_histogram: {
- field: options.sourceConfiguration.fields.timestamp,
- interval: options.timerange.interval || '1m',
- offset: getDateHistogramOffset(options.timerange.from, options.timerange.interval),
- extended_bounds: {
- min: options.timerange.from,
- max: options.timerange.to,
- },
- },
- aggregations: getMetricsAggregations(options),
- },
- },
- },
- },
- },
- };
- return getAllCompositeData(
- callClusterFactory(client),
- query,
- bucketSelector,
- handleAfterKey
- );
-};
-
-// buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[]
-// but typing this in a way that makes TypeScript happy is unreadable (if possible at all)
-interface InfraSnapshotAggregationResponse {
- nodes: {
- buckets: any[];
- after_key: { [id: string]: string };
- };
-}
-
-const mergeNodeBuckets = (
- nodeGroupByBuckets: InfraSnapshotNodeGroupByBucket[],
- nodeMetricsBuckets: InfraSnapshotNodeMetricsBucket[],
- options: InfraSnapshotRequestOptions
-): NamedSnapshotNode[] => {
- const nodeMetricsForLookup = getNodeMetricsForLookup(nodeMetricsBuckets);
-
- return nodeGroupByBuckets.map((node) => {
- return {
- name: node.key.name || node.key.id, // For type safety; name can be derived from getNodePath but not in a TS-friendly way
- path: getNodePath(node, options),
- metrics: getNodeMetrics(nodeMetricsForLookup[node.key.id], options),
- };
- });
-};
-
-const createQueryFilterClauses = (filterQuery: JsonObject | undefined) =>
- filterQuery ? [filterQuery] : [];
-
-const buildFilters = (options: InfraSnapshotRequestOptions, withQuery = true) => {
- let filters: any = [
- {
- range: {
- [options.sourceConfiguration.fields.timestamp]: {
- gte: options.timerange.from,
- lte: options.timerange.to,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- if (withQuery) {
- filters = [...createQueryFilterClauses(options.filterQuery), ...filters];
- }
-
- if (options.accountId) {
- filters.push({
- term: {
- 'cloud.account.id': options.accountId,
- },
- });
- }
-
- if (options.region) {
- filters.push({
- term: {
- 'cloud.region': options.region,
- },
- });
- }
-
- return filters;
-};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/types.ts b/x-pack/plugins/infra/server/lib/snapshot/types.ts
deleted file mode 100644
index 7e17cb91c6a59..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { JsonObject } from '../../../common/typed_json';
-import { InfraSourceConfiguration } from '../../../common/graphql/types';
-import { SnapshotRequest } from '../../../common/http_api/snapshot_api';
-
-export interface InfraSnapshotRequestOptions
- extends Omit {
- sourceConfiguration: InfraSourceConfiguration;
- filterQuery: JsonObject | undefined;
-}
diff --git a/x-pack/plugins/infra/server/lib/sources/has_data.ts b/x-pack/plugins/infra/server/lib/sources/has_data.ts
index 79b1375059dcb..53297640e541d 100644
--- a/x-pack/plugins/infra/server/lib/sources/has_data.ts
+++ b/x-pack/plugins/infra/server/lib/sources/has_data.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESSearchClient } from '../snapshot';
+import { ESSearchClient } from '../metrics/types';
export const hasData = async (index: string, client: ESSearchClient) => {
const params = {
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index 51f91d7189db7..90b73b9a7585a 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -19,7 +19,6 @@ import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_sta
import { InfraFieldsDomain } from './lib/domains/fields_domain';
import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain';
import { InfraMetricsDomain } from './lib/domains/metrics_domain';
-import { InfraSnapshot } from './lib/snapshot';
import { InfraSourceStatus } from './lib/source_status';
import { InfraSources } from './lib/sources';
import { InfraServerPluginDeps } from './lib/adapters/framework';
@@ -105,7 +104,6 @@ export class InfraServerPlugin {
sources,
}
);
- const snapshot = new InfraSnapshot();
// register saved object types
core.savedObjects.registerType(infraSourceConfigurationSavedObjectType);
@@ -129,7 +127,6 @@ export class InfraServerPlugin {
this.libs = {
configuration: this.config,
framework,
- snapshot,
sources,
sourceStatus,
...domainLibs,
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index 5594323d706de..40d09dadfe050 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -82,7 +82,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
callCluster,
params: { criteria, filterQuery, nodeType },
lookback,
- config: source.configuration,
+ source,
alertInterval,
});
diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts
index 2cd889d9c5568..c1f63d9c29577 100644
--- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts
+++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts
@@ -4,14 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-import { schema } from '@kbn/config-schema';
-
-import { throwErrors } from '../../../common/runtime_types';
+import { createValidationFunction } from '../../../common/runtime_types';
import { InfraBackendLibs } from '../../lib/infra_types';
import {
@@ -22,22 +15,16 @@ import {
import { parseFilterQuery } from '../../utils/serialized_query';
import { LogEntriesParams } from '../../lib/domains/log_entries_domain';
-const escapeHatch = schema.object({}, { unknowns: 'allow' });
-
export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ENTRIES_PATH,
- validate: { body: escapeHatch },
+ validate: { body: createValidationFunction(logEntriesRequestRT) },
},
async (requestContext, request, response) => {
try {
- const payload = pipe(
- logEntriesRequestRT.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
+ const payload = request.body;
const {
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts
index 85dba8f598a89..67ca481ff4fcb 100644
--- a/x-pack/plugins/infra/server/routes/log_entries/item.ts
+++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts
@@ -4,14 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-import { schema } from '@kbn/config-schema';
-
-import { throwErrors } from '../../../common/runtime_types';
+import { createValidationFunction } from '../../../common/runtime_types';
import { InfraBackendLibs } from '../../lib/infra_types';
import {
@@ -20,22 +13,16 @@ import {
logEntriesItemResponseRT,
} from '../../../common/http_api';
-const escapeHatch = schema.object({}, { unknowns: 'allow' });
-
export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ENTRIES_ITEM_PATH,
- validate: { body: escapeHatch },
+ validate: { body: createValidationFunction(logEntriesItemRequestRT) },
},
async (requestContext, request, response) => {
try {
- const payload = pipe(
- logEntriesItemRequestRT.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
+ const payload = request.body;
const { id, sourceId } = payload;
const sourceConfiguration = (
await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId)
diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
index 876bbb4199441..8ab0f4a44c85d 100644
--- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
+++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
@@ -7,9 +7,9 @@
import { uniq } from 'lodash';
import LRU from 'lru-cache';
import { MetricsExplorerRequestBody } from '../../../../common/http_api';
-import { ESSearchClient } from '../../../lib/snapshot';
import { getDatasetForField } from './get_dataset_for_field';
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
+import { ESSearchClient } from '../../../lib/metrics/types';
const cache = new LRU({
max: 100,
diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
index 94e91d32b14bb..85bb5b106c87c 100644
--- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
+++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESSearchClient } from '../../../lib/snapshot';
+import { ESSearchClient } from '../../../lib/metrics/types';
interface EventDatasetHit {
_source: {
diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts
index 00bc1e74ea871..3f09ae89bc97e 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts
@@ -10,10 +10,10 @@ import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { InfraBackendLibs } from '../../lib/infra_types';
import { UsageCollector } from '../../usage/usage_collector';
-import { parseFilterQuery } from '../../utils/serialized_query';
import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api';
import { throwErrors } from '../../../common/runtime_types';
import { createSearchClient } from '../../lib/create_search_client';
+import { getNodes } from './lib/get_nodes';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
@@ -30,43 +30,22 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
},
async (requestContext, request, response) => {
try {
- const {
- filterQuery,
- nodeType,
- groupBy,
- sourceId,
- metrics,
- timerange,
- accountId,
- region,
- includeTimeseries,
- overrideCompositeSize,
- } = pipe(
+ const snapshotRequest = pipe(
SnapshotRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
+
const source = await libs.sources.getSourceConfiguration(
requestContext.core.savedObjects.client,
- sourceId
+ snapshotRequest.sourceId
);
- UsageCollector.countNode(nodeType);
- const options = {
- filterQuery: parseFilterQuery(filterQuery),
- accountId,
- region,
- nodeType,
- groupBy,
- sourceConfiguration: source.configuration,
- metrics,
- timerange,
- includeTimeseries,
- overrideCompositeSize,
- };
+ UsageCollector.countNode(snapshotRequest.nodeType);
const client = createSearchClient(requestContext, framework);
- const nodesWithInterval = await libs.snapshot.getNodes(client, options);
+ const snapshotResponse = await getNodes(client, snapshotRequest, source);
+
return response.ok({
- body: SnapshotNodeResponseRT.encode(nodesWithInterval),
+ body: SnapshotNodeResponseRT.encode(snapshotResponse),
});
} catch (error) {
return response.internalError({
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts
new file mode 100644
index 0000000000000..f41d76bbc156f
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get, last, first, isArray } from 'lodash';
+import { findInventoryFields } from '../../../../common/inventory_models';
+import {
+ SnapshotRequest,
+ SnapshotNodePath,
+ SnapshotNode,
+ MetricsAPISeries,
+ MetricsAPIRow,
+} from '../../../../common/http_api';
+import { META_KEY } from './constants';
+import { InfraSource } from '../../../lib/sources';
+
+export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject);
+
+type RowWithMetadata = MetricsAPIRow & {
+ [META_KEY]: object[];
+};
+
+export const applyMetadataToLastPath = (
+ series: MetricsAPISeries,
+ node: SnapshotNode,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource
+): SnapshotNodePath[] => {
+ // First we need to find a row with metadata
+ const rowWithMeta = series.rows.find(
+ (row) => (row[META_KEY] && isArray(row[META_KEY]) && (row[META_KEY] as object[]).length) || 0
+ ) as RowWithMetadata | undefined;
+
+ if (rowWithMeta) {
+ // We need just the first doc, there should only be one
+ const firstMetaDoc = first(rowWithMeta[META_KEY]);
+ // We also need the last path to add the metadata to
+ const lastPath = last(node.path);
+ if (firstMetaDoc && lastPath) {
+ // We will need the inventory fields so we can use the field paths to get
+ // the values from the metadata document
+ const inventoryFields = findInventoryFields(
+ snapshotRequest.nodeType,
+ source.configuration.fields
+ );
+ // Set the label as the name and fallback to the id OR path.value
+ lastPath.label = get(firstMetaDoc, inventoryFields.name, lastPath.value);
+ // If the inventory fields contain an ip address, we need to try and set that
+ // on the path object. IP addersses are typically stored as multiple fields. We will
+ // use the first IPV4 address we find.
+ if (inventoryFields.ip) {
+ const ipAddresses = get(firstMetaDoc, inventoryFields.ip) as string[];
+ if (Array.isArray(ipAddresses)) {
+ lastPath.ip = ipAddresses.find(isIPv4) || null;
+ } else if (typeof ipAddresses === 'string') {
+ lastPath.ip = ipAddresses;
+ }
+ }
+ return [...node.path.slice(0, node.path.length - 1), lastPath];
+ }
+ }
+ return node.path;
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts
new file mode 100644
index 0000000000000..4218aecfe74a8
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SnapshotRequest } from '../../../../common/http_api';
+import { InfraSource } from '../../../lib/sources';
+
+export const calculateIndexPatterBasedOnMetrics = (
+ options: SnapshotRequest,
+ source: InfraSource
+) => {
+ const { metrics } = options;
+ if (metrics.every((m) => m.type === 'logRate')) {
+ return source.configuration.logAlias;
+ }
+ if (metrics.some((m) => m.type === 'logRate')) {
+ return `${source.configuration.logAlias},${source.configuration.metricAlias}`;
+ }
+ return source.configuration.metricAlias;
+};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
similarity index 85%
rename from x-pack/plugins/infra/server/lib/snapshot/index.ts
rename to x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
index 8db54da803648..563c720224435 100644
--- a/x-pack/plugins/infra/server/lib/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './snapshot';
+export const META_KEY = '__metadata__';
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts
new file mode 100644
index 0000000000000..36397862e4153
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { memoize, last, first } from 'lodash';
+import { SnapshotNode, SnapshotNodeResponse } from '../../../../common/http_api';
+
+const createMissingMetricFinder = (nodes: SnapshotNode[]) =>
+ memoize((id: string) => {
+ const nodeWithMetrics = nodes.find((node) => {
+ const lastPath = last(node.path);
+ const metric = first(node.metrics);
+ return lastPath && metric && lastPath.value === id && metric.value !== null;
+ });
+ if (nodeWithMetrics) {
+ return nodeWithMetrics.metrics;
+ }
+ });
+
+/**
+ * This function will look for nodes with missing data and try to find a node to copy the data from.
+ * This functionality exists to suppor the use case where the user requests a group by on "Service type".
+ * Since that grouping naturally excludeds every metric (except the metric for the service.type), we still
+ * want to display the node with a value. A good example is viewing hosts by CPU Usage and grouping by service
+ * Without this every service but `system` would be null.
+ */
+export const copyMissingMetrics = (response: SnapshotNodeResponse) => {
+ const { nodes } = response;
+ const find = createMissingMetricFinder(nodes);
+ const newNodes = nodes.map((node) => {
+ const lastPath = last(node.path);
+ const metric = first(node.metrics);
+ const allRowsNull = metric?.timeseries?.rows.every((r) => r.metric_0 == null) ?? true;
+ if (lastPath && metric && metric.value === null && allRowsNull) {
+ const newMetrics = find(lastPath.value);
+ if (newMetrics) {
+ return { ...node, metrics: newMetrics };
+ }
+ }
+ return node;
+ });
+ return { ...response, nodes: newNodes };
+};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
similarity index 80%
rename from x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts
rename to x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
index 719ffdb8fa7c4..827e0901c1c01 100644
--- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
@@ -5,14 +5,16 @@
*/
import { uniq } from 'lodash';
-import { InfraSnapshotRequestOptions } from './types';
-import { getMetricsAggregations } from './query_helpers';
-import { calculateMetricInterval } from '../../utils/calculate_metric_interval';
-import { MetricsUIAggregation, ESBasicMetricAggRT } from '../../../common/inventory_models/types';
-import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field';
-import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api';
-import { ESSearchClient } from '.';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
+import { InfraTimerangeInput } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
+import { getMetricsAggregations, InfraSnapshotRequestOptions } from './get_metrics_aggregations';
+import {
+ MetricsUIAggregation,
+ ESBasicMetricAggRT,
+} from '../../../../common/inventory_models/types';
+import { getDatasetForField } from '../../metrics_explorer/lib/get_dataset_for_field';
const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => {
const { timerange } = options;
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts
new file mode 100644
index 0000000000000..2421469eb1bdd
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { JsonObject } from '../../../../common/typed_json';
+import {
+ InventoryItemType,
+ MetricsUIAggregation,
+ MetricsUIAggregationRT,
+} from '../../../../common/inventory_models/types';
+import {
+ SnapshotMetricInput,
+ SnapshotCustomMetricInputRT,
+ SnapshotRequest,
+} from '../../../../common/http_api';
+import { findInventoryModel } from '../../../../common/inventory_models';
+import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+
+export interface InfraSnapshotRequestOptions
+ extends Omit {
+ sourceConfiguration: InfraSourceConfiguration;
+ filterQuery: JsonObject | undefined;
+}
+
+export const metricToAggregation = (
+ nodeType: InventoryItemType,
+ metric: SnapshotMetricInput,
+ index: number
+) => {
+ const inventoryModel = findInventoryModel(nodeType);
+ if (SnapshotCustomMetricInputRT.is(metric)) {
+ if (metric.aggregation === 'rate') {
+ return networkTraffic(`custom_${index}`, metric.field);
+ }
+ return {
+ [`custom_${index}`]: {
+ [metric.aggregation]: {
+ field: metric.field,
+ },
+ },
+ };
+ }
+ return inventoryModel.metrics.snapshot?.[metric.type];
+};
+
+export const getMetricsAggregations = (
+ options: InfraSnapshotRequestOptions
+): MetricsUIAggregation => {
+ const { metrics } = options;
+ return metrics.reduce((aggs, metric, index) => {
+ const aggregation = metricToAggregation(options.nodeType, metric, index);
+ if (!MetricsUIAggregationRT.is(aggregation)) {
+ throw new Error(
+ i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
+ defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
+ values: {
+ nodeType: options.nodeType,
+ metric: metric.type,
+ },
+ })
+ );
+ }
+ return { ...aggs, ...aggregation };
+ }, {});
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
new file mode 100644
index 0000000000000..9332d5aee1f52
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SnapshotRequest } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { InfraSource } from '../../../lib/sources';
+import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request';
+import { queryAllData } from './query_all_data';
+import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response';
+import { copyMissingMetrics } from './copy_missing_metrics';
+
+export const getNodes = async (
+ client: ESSearchClient,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource
+) => {
+ const metricsApiRequest = await transformRequestToMetricsAPIRequest(
+ client,
+ source,
+ snapshotRequest
+ );
+ const metricsApiResponse = await queryAllData(client, metricsApiRequest);
+ return copyMissingMetrics(
+ transformMetricsApiResponseToSnapshotResponse(
+ metricsApiRequest,
+ snapshotRequest,
+ source,
+ metricsApiResponse
+ )
+ );
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts
new file mode 100644
index 0000000000000..a9d2352cf55b7
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MetricsAPIRequest, MetricsAPIResponse } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { query } from '../../../lib/metrics';
+
+const handleResponse = (
+ client: ESSearchClient,
+ options: MetricsAPIRequest,
+ previousResponse?: MetricsAPIResponse
+) => async (resp: MetricsAPIResponse): Promise => {
+ const combinedResponse = previousResponse
+ ? {
+ ...previousResponse,
+ series: [...previousResponse.series, ...resp.series],
+ info: resp.info,
+ }
+ : resp;
+ if (resp.info.afterKey) {
+ return query(client, { ...options, afterKey: resp.info.afterKey }).then(
+ handleResponse(client, options, combinedResponse)
+ );
+ }
+ return combinedResponse;
+};
+
+export const queryAllData = (client: ESSearchClient, options: MetricsAPIRequest) => {
+ return query(client, options).then(handleResponse(client, options));
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
new file mode 100644
index 0000000000000..700f4ef39bb66
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { findInventoryFields } from '../../../../common/inventory_models';
+import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { InfraSource } from '../../../lib/sources';
+import { createTimeRangeWithInterval } from './create_timerange_with_interval';
+import { parseFilterQuery } from '../../../utils/serialized_query';
+import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapshot_metrics_to_metrics_api_metrics';
+import { calculateIndexPatterBasedOnMetrics } from './calculate_index_pattern_based_on_metrics';
+import { META_KEY } from './constants';
+
+export const transformRequestToMetricsAPIRequest = async (
+ client: ESSearchClient,
+ source: InfraSource,
+ snapshotRequest: SnapshotRequest
+): Promise => {
+ const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, {
+ ...snapshotRequest,
+ filterQuery: parseFilterQuery(snapshotRequest.filterQuery),
+ sourceConfiguration: source.configuration,
+ });
+
+ const metricsApiRequest: MetricsAPIRequest = {
+ indexPattern: calculateIndexPatterBasedOnMetrics(snapshotRequest, source),
+ timerange: {
+ field: source.configuration.fields.timestamp,
+ from: timeRangeWithIntervalApplied.from,
+ to: timeRangeWithIntervalApplied.to,
+ interval: timeRangeWithIntervalApplied.interval,
+ },
+ metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest),
+ limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 10,
+ alignDataToEnd: true,
+ };
+
+ const filters = [];
+ const parsedFilters = parseFilterQuery(snapshotRequest.filterQuery);
+ if (parsedFilters) {
+ filters.push(parsedFilters);
+ }
+
+ if (snapshotRequest.accountId) {
+ filters.push({ term: { 'cloud.account.id': snapshotRequest.accountId } });
+ }
+
+ if (snapshotRequest.region) {
+ filters.push({ term: { 'cloud.region': snapshotRequest.region } });
+ }
+
+ const inventoryFields = findInventoryFields(
+ snapshotRequest.nodeType,
+ source.configuration.fields
+ );
+ const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[];
+ metricsApiRequest.groupBy = [...groupBy, inventoryFields.id];
+
+ const metaAggregation = {
+ id: META_KEY,
+ aggregations: {
+ [META_KEY]: {
+ top_hits: {
+ size: 1,
+ _source: [inventoryFields.name],
+ sort: [{ [source.configuration.fields.timestamp]: 'desc' }],
+ },
+ },
+ },
+ };
+ if (inventoryFields.ip) {
+ metaAggregation.aggregations[META_KEY].top_hits._source.push(inventoryFields.ip);
+ }
+ metricsApiRequest.metrics.push(metaAggregation);
+
+ if (filters.length) {
+ metricsApiRequest.filters = filters;
+ }
+
+ return metricsApiRequest;
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts
new file mode 100644
index 0000000000000..6f7c88eda5d7a
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
+import { findInventoryModel } from '../../../../common/inventory_models';
+import {
+ MetricsAPIMetric,
+ SnapshotRequest,
+ SnapshotCustomMetricInputRT,
+} from '../../../../common/http_api';
+
+export const transformSnapshotMetricsToMetricsAPIMetrics = (
+ snapshotRequest: SnapshotRequest
+): MetricsAPIMetric[] => {
+ return snapshotRequest.metrics.map((metric, index) => {
+ const inventoryModel = findInventoryModel(snapshotRequest.nodeType);
+ if (SnapshotCustomMetricInputRT.is(metric)) {
+ const customId = `custom_${index}`;
+ if (metric.aggregation === 'rate') {
+ return { id: customId, aggregations: networkTraffic(customId, metric.field) };
+ }
+ return {
+ id: customId,
+ aggregations: {
+ [customId]: {
+ [metric.aggregation]: {
+ field: metric.field,
+ },
+ },
+ },
+ };
+ }
+ return { id: metric.type, aggregations: inventoryModel.metrics.snapshot?.[metric.type] };
+ });
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts
new file mode 100644
index 0000000000000..309598d71c361
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get, max, sum, last, isNumber } from 'lodash';
+import { SnapshotMetricType } from '../../../../common/inventory_models/types';
+import {
+ MetricsAPIResponse,
+ SnapshotNodeResponse,
+ MetricsAPIRequest,
+ MetricsExplorerColumnType,
+ MetricsAPIRow,
+ SnapshotRequest,
+ SnapshotNodePath,
+ SnapshotNodeMetric,
+} from '../../../../common/http_api';
+import { META_KEY } from './constants';
+import { InfraSource } from '../../../lib/sources';
+import { applyMetadataToLastPath } from './apply_metadata_to_last_path';
+
+const getMetricValue = (row: MetricsAPIRow) => {
+ if (!isNumber(row.metric_0)) return null;
+ const value = row.metric_0;
+ return isFinite(value) ? value : null;
+};
+
+const calculateMax = (rows: MetricsAPIRow[]) => {
+ return max(rows.map(getMetricValue)) || 0;
+};
+
+const calculateAvg = (rows: MetricsAPIRow[]): number => {
+ return sum(rows.map(getMetricValue)) / rows.length || 0;
+};
+
+const getLastValue = (rows: MetricsAPIRow[]) => {
+ const row = last(rows);
+ if (!row) return null;
+ return getMetricValue(row);
+};
+
+export const transformMetricsApiResponseToSnapshotResponse = (
+ options: MetricsAPIRequest,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource,
+ metricsApiResponse: MetricsAPIResponse
+): SnapshotNodeResponse => {
+ const nodes = metricsApiResponse.series.map((series) => {
+ const node = {
+ metrics: options.metrics
+ .filter((m) => m.id !== META_KEY)
+ .map((metric) => {
+ const name = metric.id as SnapshotMetricType;
+ const timeseries = {
+ id: name,
+ columns: [
+ { name: 'timestamp', type: 'date' as MetricsExplorerColumnType },
+ { name: 'metric_0', type: 'number' as MetricsExplorerColumnType },
+ ],
+ rows: series.rows.map((row) => {
+ return { timestamp: row.timestamp, metric_0: get(row, metric.id, null) };
+ }),
+ };
+ const maxValue = calculateMax(timeseries.rows);
+ const avg = calculateAvg(timeseries.rows);
+ const value = getLastValue(timeseries.rows);
+ const nodeMetric: SnapshotNodeMetric = { name, max: maxValue, value, avg };
+ if (snapshotRequest.includeTimeseries) {
+ nodeMetric.timeseries = timeseries;
+ }
+ return nodeMetric;
+ }),
+ path:
+ series.keys?.map((key) => {
+ return { value: key, label: key } as SnapshotNodePath;
+ }) ?? [],
+ name: '',
+ };
+
+ const path = applyMetadataToLastPath(series, node, snapshotRequest, source);
+ const lastPath = last(path);
+ const name = (lastPath && lastPath.label) || 'N/A';
+ return { ...node, path, name };
+ });
+ return { nodes, interval: `${metricsApiResponse.info.interval}s` };
+};
diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
index a3d674b324ae8..6d16e045d26d5 100644
--- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
+++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
@@ -8,7 +8,7 @@
import { findInventoryModel } from '../../common/inventory_models';
// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
import { InventoryItemType } from '../../common/inventory_models/types';
-import { ESSearchClient } from '../lib/snapshot';
+import { ESSearchClient } from '../lib/metrics/types';
interface Options {
indexPattern: string;
diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
index d75a914e080d7..b7856e6d57402 100644
--- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
+++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
@@ -1425,11 +1425,13 @@
},
"icons": [
{
- "src": "/package/coredns-1.0.1/img/icon.png",
+ "path": "/package/coredns-1.0.1/img/icon.png",
+ "src": "/img/icon.png",
"size": "1800x1800"
},
{
- "src": "/package/coredns-1.0.1/img/icon.svg",
+ "path": "/package/coredns-1.0.1/img/icon.svg",
+ "src": "/img/icon.svg",
"size": "255x144",
"type": "image/svg+xml"
}
@@ -1704,7 +1706,8 @@
},
"icons": [
{
- "src": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg",
+ "path": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg",
+ "src": "/img/logo-endpoint-64-color.svg",
"size": "16x16",
"type": "image/svg+xml"
}
@@ -2001,7 +2004,8 @@
"download": "/epr/aws/aws-0.0.3.tar.gz",
"icons": [
{
- "src": "/package/aws/0.0.3/img/logo_aws.svg",
+ "path": "/package/aws/0.0.3/img/logo_aws.svg",
+ "src": "/img/logo_aws.svg",
"title": "logo aws",
"size": "32x32",
"type": "image/svg+xml"
@@ -2019,7 +2023,8 @@
"download": "/epr/endpoint/endpoint-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg",
+ "path": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg",
+ "src": "/img/logo-endpoint-64-color.svg",
"size": "16x16",
"type": "image/svg+xml"
}
@@ -2087,7 +2092,8 @@
"download": "/epr/log/log-0.9.0.tar.gz",
"icons": [
{
- "src": "/package/log/0.9.0/img/icon.svg",
+ "path": "/package/log/0.9.0/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2103,7 +2109,8 @@
"download": "/epr/longdocs/longdocs-1.0.4.tar.gz",
"icons": [
{
- "src": "/package/longdocs/1.0.4/img/icon.svg",
+ "path": "/package/longdocs/1.0.4/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2119,7 +2126,8 @@
"download": "/epr/metricsonly/metricsonly-2.0.1.tar.gz",
"icons": [
{
- "src": "/package/metricsonly/2.0.1/img/icon.svg",
+ "path": "/package/metricsonly/2.0.1/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2135,7 +2143,8 @@
"download": "/epr/multiversion/multiversion-1.1.0.tar.gz",
"icons": [
{
- "src": "/package/multiversion/1.1.0/img/icon.svg",
+ "path": "/package/multiversion/1.1.0/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2151,7 +2160,8 @@
"download": "/epr/mysql/mysql-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/mysql/0.1.0/img/logo_mysql.svg",
+ "path": "/package/mysql/0.1.0/img/logo_mysql.svg",
+ "src": "/img/logo_mysql.svg",
"title": "logo mysql",
"size": "32x32",
"type": "image/svg+xml"
@@ -2169,7 +2179,8 @@
"download": "/epr/nginx/nginx-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/nginx/0.1.0/img/logo_nginx.svg",
+ "path": "/package/nginx/0.1.0/img/logo_nginx.svg",
+ "src": "/img/logo_nginx.svg",
"title": "logo nginx",
"size": "32x32",
"type": "image/svg+xml"
@@ -2187,7 +2198,8 @@
"download": "/epr/redis/redis-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/redis/0.1.0/img/logo_redis.svg",
+ "path": "/package/redis/0.1.0/img/logo_redis.svg",
+ "src": "/img/logo_redis.svg",
"title": "logo redis",
"size": "32x32",
"type": "image/svg+xml"
@@ -2205,7 +2217,8 @@
"download": "/epr/reference/reference-1.0.0.tar.gz",
"icons": [
{
- "src": "/package/reference/1.0.0/img/icon.svg",
+ "path": "/package/reference/1.0.0/img/icon.svg",
+ "src": "/img/icon.svg",
"size": "32x32",
"type": "image/svg+xml"
}
@@ -2222,7 +2235,8 @@
"download": "/epr/system/system-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/system/0.1.0/img/system.svg",
+ "path": "/package/system/0.1.0/img/system.svg",
+ "src": "/img/system.svg",
"title": "system",
"size": "1000x1000",
"type": "image/svg+xml"
@@ -3913,11 +3927,20 @@
"src": {
"type": "string"
},
+ "path": {
+ "type": "string"
+ },
"title": {
"type": "string"
+ },
+ "size": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
}
},
- "required": ["src"]
+ "required": ["src", "path"]
}
},
"icons": {
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 140a76ac85e61..8bc5d9f7210b2 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -19,6 +19,8 @@ export enum InstallStatus {
uninstalling = 'uninstalling',
}
+export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install';
+
export type EpmPackageInstallStatus = 'installed' | 'installing';
export type DetailViewPanelName = 'overview' | 'usages' | 'settings';
@@ -38,6 +40,7 @@ export enum ElasticsearchAssetType {
ingestPipeline = 'ingest_pipeline',
indexTemplate = 'index_template',
ilmPolicy = 'ilm_policy',
+ transform = 'transform',
}
export enum AgentAssetType {
@@ -71,10 +74,8 @@ export interface RegistryPackage {
}
interface RegistryImage {
- // https://github.com/elastic/package-registry/blob/master/util/package.go#L74
- // says src is potentially missing but I couldn't find any examples
- // it seems like src should be required. How can you have an image with no reference to the content?
src: string;
+ path: string;
title?: string;
size?: string;
type?: string;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
index e5a7191372e9c..690ffdf46f704 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
@@ -42,7 +42,7 @@ export const usePackageIconType = ({
const svgIcons = (paramIcons || iconList)?.filter(
(iconDef) => iconDef.type === 'image/svg+xml'
);
- const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src;
+ const localIconSrc = Array.isArray(svgIcons) && (svgIcons[0].path || svgIcons[0].src);
if (localIconSrc) {
CACHED_ICONS.set(pkgKey, toImage(localIconSrc));
setIconType(CACHED_ICONS.get(pkgKey) || '');
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
index 31c6d76446447..da3cab1a4b8a3 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
@@ -19,6 +19,7 @@ export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
+ transform: 'Transform',
'index-pattern': 'Index Pattern',
index_template: 'Index Template',
component_template: 'Component Template',
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
index d8388a71556d6..6326e9072be8e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
@@ -75,7 +75,7 @@ export function Screenshots(props: ScreenshotProps) {
set image to same width. Will need to update if size changes.
*/}
{
+ return Registry.getAsset(path);
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
new file mode 100644
index 0000000000000..1e58319183c7d
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+
+import { saveInstalledEsRefs } from '../../packages/install';
+import * as Registry from '../../registry';
+import {
+ Dataset,
+ ElasticsearchAssetType,
+ EsAssetReference,
+ RegistryPackage,
+} from '../../../../../common/types/models';
+import { CallESAsCurrentUser } from '../../../../types';
+import { getInstallation } from '../../packages';
+import { deleteTransforms, deleteTransformRefs } from './remove';
+import { getAsset } from './common';
+
+interface TransformInstallation {
+ installationName: string;
+ content: string;
+}
+
+interface TransformPathDataset {
+ path: string;
+ dataset: Dataset;
+}
+
+export const installTransformForDataset = async (
+ registryPackage: RegistryPackage,
+ paths: string[],
+ callCluster: CallESAsCurrentUser,
+ savedObjectsClient: SavedObjectsClientContract
+) => {
+ const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name });
+ let previousInstalledTransformEsAssets: EsAssetReference[] = [];
+ if (installation) {
+ previousInstalledTransformEsAssets = installation.installed_es.filter(
+ ({ type, id }) => type === ElasticsearchAssetType.transform
+ );
+ }
+
+ // delete all previous transform
+ await deleteTransforms(
+ callCluster,
+ previousInstalledTransformEsAssets.map((asset) => asset.id)
+ );
+ // install the latest dataset
+ const datasets = registryPackage.datasets;
+ if (!datasets?.length) return [];
+ const installNameSuffix = `${registryPackage.version}`;
+
+ const transformPaths = paths.filter((path) => isTransform(path));
+ let installedTransforms: EsAssetReference[] = [];
+ if (transformPaths.length > 0) {
+ const transformPathDatasets = datasets.reduce((acc, dataset) => {
+ transformPaths.forEach((path) => {
+ if (isDatasetTransform(path, dataset.path)) {
+ acc.push({ path, dataset });
+ }
+ });
+ return acc;
+ }, []);
+
+ const transformRefs = transformPathDatasets.reduce(
+ (acc, transformPathDataset) => {
+ if (transformPathDataset) {
+ acc.push({
+ id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
+ type: ElasticsearchAssetType.transform,
+ });
+ }
+ return acc;
+ },
+ []
+ );
+
+ // get and save transform refs before installing transforms
+ await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
+
+ const transforms: TransformInstallation[] = transformPathDatasets.map(
+ (transformPathDataset: TransformPathDataset) => {
+ return {
+ installationName: getTransformNameForInstallation(
+ transformPathDataset,
+ installNameSuffix
+ ),
+ content: getAsset(transformPathDataset.path).toString('utf-8'),
+ };
+ }
+ );
+
+ const installationPromises = transforms.map(async (transform) => {
+ return installTransform({ callCluster, transform });
+ });
+
+ installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
+ }
+
+ if (previousInstalledTransformEsAssets.length > 0) {
+ const currentInstallation = await getInstallation({
+ savedObjectsClient,
+ pkgName: registryPackage.name,
+ });
+
+ // remove the saved object reference
+ await deleteTransformRefs(
+ savedObjectsClient,
+ currentInstallation?.installed_es || [],
+ registryPackage.name,
+ previousInstalledTransformEsAssets.map((asset) => asset.id),
+ installedTransforms.map((installed) => installed.id)
+ );
+ }
+ return installedTransforms;
+};
+
+const isTransform = (path: string) => {
+ const pathParts = Registry.pathParts(path);
+ return pathParts.type === ElasticsearchAssetType.transform;
+};
+
+const isDatasetTransform = (path: string, datasetName: string) => {
+ const pathParts = Registry.pathParts(path);
+ return (
+ !path.endsWith('/') &&
+ pathParts.type === ElasticsearchAssetType.transform &&
+ pathParts.dataset !== undefined &&
+ datasetName === pathParts.dataset
+ );
+};
+
+async function installTransform({
+ callCluster,
+ transform,
+}: {
+ callCluster: CallESAsCurrentUser;
+ transform: TransformInstallation;
+}): Promise {
+ // defer validation on put if the source index is not available
+ await callCluster('transport.request', {
+ method: 'PUT',
+ path: `_transform/${transform.installationName}`,
+ query: 'defer_validation=true',
+ body: transform.content,
+ });
+
+ await callCluster('transport.request', {
+ method: 'POST',
+ path: `_transform/${transform.installationName}/_start`,
+ });
+
+ return { id: transform.installationName, type: ElasticsearchAssetType.transform };
+}
+
+const getTransformNameForInstallation = (
+ transformDataset: TransformPathDataset,
+ suffix: string
+) => {
+ const filename = transformDataset?.path.split('/')?.pop()?.split('.')[0];
+ return `${transformDataset.dataset.type}-${transformDataset.dataset.name}-${filename}-${suffix}`;
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts
new file mode 100644
index 0000000000000..3f85ee9b550b2
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { deleteTransformRefs } from './remove';
+import { EsAssetReference } from '../../../../../common/types/models';
+
+describe('test transform install', () => {
+ let savedObjectsClient: jest.Mocked;
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ });
+
+ test('can delete transform ref and handle duplicate when previous version and current version are the same', async () => {
+ await deleteTransformRefs(
+ savedObjectsClient,
+ [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ] as EsAssetReference[],
+ 'endpoint',
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0'],
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0']
+ );
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can delete transform ref when previous version and current version are not the same', async () => {
+ await deleteTransformRefs(
+ savedObjectsClient,
+ [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ] as EsAssetReference[],
+ 'endpoint',
+ ['metrics-endpoint.metadata-current-default-0.15.0-dev.0'],
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0']
+ );
+
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts
new file mode 100644
index 0000000000000..5c9d3e2846200
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types';
+import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants';
+
+export const stopTransforms = async (transformIds: string[], callCluster: CallESAsCurrentUser) => {
+ for (const transformId of transformIds) {
+ await callCluster('transport.request', {
+ method: 'POST',
+ path: `_transform/${transformId}/_stop`,
+ query: 'force=true',
+ ignore: [404],
+ });
+ }
+};
+
+export const deleteTransforms = async (
+ callCluster: CallESAsCurrentUser,
+ transformIds: string[]
+) => {
+ await Promise.all(
+ transformIds.map(async (transformId) => {
+ await stopTransforms([transformId], callCluster);
+ await callCluster('transport.request', {
+ method: 'DELETE',
+ query: 'force=true',
+ path: `_transform/${transformId}`,
+ ignore: [404],
+ });
+ })
+ );
+};
+
+export const deleteTransformRefs = async (
+ savedObjectsClient: SavedObjectsClientContract,
+ installedEsAssets: EsAssetReference[],
+ pkgName: string,
+ installedEsIdToRemove: string[],
+ currentInstalledEsTransformIds: string[]
+) => {
+ const seen = new Set();
+ const filteredAssets = installedEsAssets.filter(({ type, id }) => {
+ if (type !== ElasticsearchAssetType.transform) return true;
+ const add =
+ (currentInstalledEsTransformIds.includes(id) || !installedEsIdToRemove.includes(id)) &&
+ !seen.has(id);
+ seen.add(id);
+ return add;
+ });
+ return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
+ installed_es: filteredAssets,
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
new file mode 100644
index 0000000000000..0b66077b8699a
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
@@ -0,0 +1,420 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../packages/get', () => {
+ return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
+});
+
+jest.mock('./common', () => {
+ return {
+ getAsset: jest.fn(),
+ };
+});
+
+import { installTransformForDataset } from './install';
+import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server';
+import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
+import { getInstallation, getInstallationObject } from '../../packages';
+import { getAsset } from './common';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+
+describe('test transform install', () => {
+ let legacyScopedClusterClient: jest.Mocked;
+ let savedObjectsClient: jest.Mocked;
+ beforeEach(() => {
+ legacyScopedClusterClient = {
+ callAsInternalUser: jest.fn(),
+ callAsCurrentUser: jest.fn(),
+ };
+ (getInstallation as jest.MockedFunction).mockReset();
+ (getInstallationObject as jest.MockedFunction).mockReset();
+ savedObjectsClient = savedObjectsClientMock.create();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('can install new versions and removes older version', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+ (getAsset as jest.MockedFunction)
+ .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8'))
+ .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8'));
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({
+ attributes: {
+ installed_es: previousInstallation.installed_es,
+ },
+ } as unknown) as SavedObject)
+ );
+
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata',
+ },
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata Current',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ [
+ 'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json',
+ 'endpoint-0.16.0-dev.0/dataset/metadata/elasticsearch/transform/default.json',
+ 'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json',
+ ],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0/_stop',
+ query: 'force=true',
+ ignore: [404],
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'DELETE',
+ query: 'force=true',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ ignore: [404],
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0/_start',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
+ },
+ ],
+ ]);
+
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: 'ingest_pipeline',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ ],
+ },
+ ],
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: 'ingest_pipeline',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can install new version and when no older version', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+ (getAsset as jest.MockedFunction).mockReturnValueOnce(
+ Buffer.from('{"content": "data"}', 'utf8')
+ );
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({ attributes: { installed_es: [] } } as unknown) as SavedObject<
+ Installation
+ >)
+ );
+ legacyScopedClusterClient.callAsCurrentUser = jest.fn();
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ ['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
+ },
+ ],
+ ]);
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can removes older version when no new install in package', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.metadata-current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [],
+ } as unknown) as Installation;
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({
+ attributes: { installed_es: currentInstallation.installed_es },
+ } as unknown) as SavedObject)
+ );
+
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata',
+ },
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata Current',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ [],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ ignore: [404],
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0/_stop',
+ query: 'force=true',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ ignore: [404],
+ method: 'DELETE',
+ path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0',
+ query: 'force=true',
+ },
+ ],
+ ]);
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [],
+ },
+ ],
+ ]);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts
new file mode 100644
index 0000000000000..cc26e631a6215
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types';
+import { SavedObject } from 'src/core/server';
+import { getInstallType } from './install';
+
+const mockInstallation: SavedObject = {
+ id: 'test-pkg',
+ references: [],
+ type: 'epm-packages',
+ attributes: {
+ id: 'test-pkg',
+ installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
+ installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
+ es_index_patterns: { pattern: 'pattern-name' },
+ name: 'test packagek',
+ version: '1.0.0',
+ install_status: 'installed',
+ install_version: '1.0.0',
+ install_started_at: new Date().toISOString(),
+ },
+};
+const mockInstallationUpdateFail: SavedObject = {
+ id: 'test-pkg',
+ references: [],
+ type: 'epm-packages',
+ attributes: {
+ id: 'test-pkg',
+ installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
+ installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
+ es_index_patterns: { pattern: 'pattern-name' },
+ name: 'test packagek',
+ version: '1.0.0',
+ install_status: 'installing',
+ install_version: '1.0.1',
+ install_started_at: new Date().toISOString(),
+ },
+};
+describe('install', () => {
+ describe('getInstallType', () => {
+ it('should return correct type when installing and no other version is currently installed', () => {});
+ const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined });
+ expect(installTypeInstall).toBe('install');
+
+ it('should return correct type when installing the same version', () => {});
+ const installTypeReinstall = getInstallType({
+ pkgVersion: '1.0.0',
+ installedPkg: mockInstallation,
+ });
+ expect(installTypeReinstall).toBe('reinstall');
+
+ it('should return correct type when moving from one version to another', () => {});
+ const installTypeUpdate = getInstallType({
+ pkgVersion: '1.0.1',
+ installedPkg: mockInstallation,
+ });
+ expect(installTypeUpdate).toBe('update');
+
+ it('should return correct type when update fails and trys again', () => {});
+ const installTypeReupdate = getInstallType({
+ pkgVersion: '1.0.1',
+ installedPkg: mockInstallationUpdateFail,
+ });
+ expect(installTypeReupdate).toBe('reupdate');
+
+ it('should return correct type when attempting to rollback from a failed update', () => {});
+ const installTypeRollback = getInstallType({
+ pkgVersion: '1.0.0',
+ installedPkg: mockInstallationUpdateFail,
+ });
+ expect(installTypeRollback).toBe('rollback');
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index e49dbe8f0b5d4..e6144e0309594 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectsClientContract } from 'src/core/server';
+import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import semver from 'semver';
import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants';
import {
@@ -16,6 +16,7 @@ import {
KibanaAssetReference,
EsAssetReference,
ElasticsearchAssetType,
+ InstallType,
} from '../../../types';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
@@ -34,6 +35,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { PackageOutdatedError } from '../../../errors';
import { getPackageSavedObjects } from './get';
+import { installTransformForDataset } from '../elasticsearch/transform/install';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -110,11 +112,13 @@ export async function installPackage({
const latestPackage = await Registry.fetchFindLatestPackage(pkgName);
// get the currently installed package
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
- const reinstall = pkgVersion === installedPkg?.attributes.version;
- const reupdate = pkgVersion === installedPkg?.attributes.install_version;
- // let the user install if using the force flag or this is a reinstall or reupdate due to intallation interruption
- if (semver.lt(pkgVersion, latestPackage.version) && !force && !reinstall && !reupdate) {
+ const installType = getInstallType({ pkgVersion, installedPkg });
+
+ // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update
+ const installOutOfDateVersionOk =
+ installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback';
+ if (semver.lt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) {
throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`);
}
const paths = await Registry.getArchiveInfo(pkgName, pkgVersion);
@@ -188,28 +192,51 @@ export async function installPackage({
// update current backing indices of each data stream
await updateCurrentWriteIndices(callCluster, installedTemplates);
- // if this is an update, delete the previous version's pipelines
- if (installedPkg && !reinstall) {
+ const installedTransforms = await installTransformForDataset(
+ registryPackageInfo,
+ paths,
+ callCluster,
+ savedObjectsClient
+ );
+
+ // if this is an update or retrying an update, delete the previous version's pipelines
+ if (installType === 'update' || installType === 'reupdate') {
await deletePreviousPipelines(
callCluster,
savedObjectsClient,
pkgName,
+ // @ts-ignore installType conditions already check for existence of installedPkg
installedPkg.attributes.version
);
}
-
+ // pipelines from a different version may have installed during a failed update
+ if (installType === 'rollback') {
+ await deletePreviousPipelines(
+ callCluster,
+ savedObjectsClient,
+ pkgName,
+ // @ts-ignore installType conditions already check for existence of installedPkg
+ installedPkg.attributes.install_version
+ );
+ }
const installedTemplateRefs = installedTemplates.map((template) => ({
id: template.templateName,
type: ElasticsearchAssetType.indexTemplate,
}));
await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);
+
// update to newly installed version when all assets are successfully installed
if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion);
await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
install_version: pkgVersion,
install_status: 'installed',
});
- return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs];
+ return [
+ ...installedKibanaAssetsRefs,
+ ...installedPipelines,
+ ...installedTemplateRefs,
+ ...installedTransforms,
+ ];
}
const updateVersion = async (
@@ -326,3 +353,23 @@ export async function ensurePackagesCompletedInstall(
await Promise.all(installingPromises);
return installingPackages;
}
+
+export function getInstallType({
+ pkgVersion,
+ installedPkg,
+}: {
+ pkgVersion: string;
+ installedPkg: SavedObject | undefined;
+}): InstallType {
+ const isInstalledPkg = !!installedPkg;
+ const currentPkgVersion = installedPkg?.attributes.version;
+ const lastStartedInstallVersion = installedPkg?.attributes.install_version;
+ if (!isInstalledPkg) return 'install';
+ if (pkgVersion === currentPkgVersion && pkgVersion !== lastStartedInstallVersion)
+ return 'rollback';
+ if (pkgVersion === currentPkgVersion) return 'reinstall';
+ if (pkgVersion === lastStartedInstallVersion && pkgVersion !== currentPkgVersion)
+ return 'reupdate';
+ if (pkgVersion !== lastStartedInstallVersion && pkgVersion !== currentPkgVersion) return 'update';
+ throw new Error('unknown install type');
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index 71eee1ee82c90..2434ebf27aa5d 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -6,12 +6,17 @@
import { SavedObjectsClientContract } from 'src/core/server';
import Boom from 'boom';
-import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../constants';
-import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types';
-import { CallESAsCurrentUser } from '../../../types';
+import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
+import {
+ AssetReference,
+ AssetType,
+ CallESAsCurrentUser,
+ ElasticsearchAssetType,
+} from '../../../types';
import { getInstallation, savedObjectTypes } from './index';
import { deletePipeline } from '../elasticsearch/ingest_pipeline/';
import { installIndexPatterns } from '../kibana/index_pattern/install';
+import { deleteTransforms } from '../elasticsearch/transform/remove';
import { packagePolicyService, appContextService } from '../..';
import { splitPkgKey, deletePackageCache, getArchiveInfo } from '../registry';
@@ -72,6 +77,8 @@ async function deleteAssets(
return deletePipeline(callCluster, id);
} else if (assetType === ElasticsearchAssetType.indexTemplate) {
return deleteTemplate(callCluster, id);
+ } else if (assetType === ElasticsearchAssetType.transform) {
+ return deleteTransforms(callCluster, [id]);
}
});
try {
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index e01568cfbb3c9..2746dfcd00ce3 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -63,6 +63,7 @@ export {
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
+ InstallType,
// Agent Request types
PostAgentEnrollRequest,
PostAgentCheckinRequest,
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
index a42df6873d57b..2f2a75853d9e9 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
@@ -87,17 +85,7 @@ export const Gsub: FunctionComponent = () => {
- {'field'},
- }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
index fb1a2d97672b0..c3f38cb021371 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FieldNameField } from './common_fields/field_name_field';
import { IgnoreMissingField } from './common_fields/ignore_missing_field';
@@ -23,15 +21,7 @@ export const HtmlStrip: FunctionComponent = () => {
)}
/>
- {'field'} }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
index ab077d3337f63..c70f48e0297e4 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
@@ -55,17 +53,7 @@ export const Join: FunctionComponent = () => {
- {'field'},
- }}
- />
- }
- />
+
>
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
index b68b398325085..f01228a26297b 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
@@ -65,12 +65,7 @@ export const Json: FunctionComponent = () => {
)}
/>
-
+
{'" "'},
+ }}
+ />
+ ),
validations: [
{
validator: emptyField(
@@ -52,9 +58,15 @@ const fieldsConfig: FieldsConfig = {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', {
defaultMessage: 'Value split',
}),
- helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', {
- defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.',
- }),
+ helpText: (
+ {'"="'},
+ }}
+ />
+ ),
validations: [
{
validator: emptyField(
@@ -75,8 +87,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Include keys',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', {
- defaultMessage:
- 'List of keys to filter and insert into document. Defaults to including all keys.',
+ defaultMessage: 'List of extracted keys to include in the output. Defaults to all keys.',
}),
},
@@ -88,7 +99,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Exclude keys',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', {
- defaultMessage: 'List of keys to exclude from document.',
+ defaultMessage: 'List of extracted keys to exclude from the output.',
}),
},
@@ -99,7 +110,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Prefix',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', {
- defaultMessage: 'Prefix to be added to extracted keys.',
+ defaultMessage: 'Prefix to add to extracted keys.',
}),
},
@@ -136,7 +147,7 @@ const fieldsConfig: FieldsConfig = {
helpText: (
{'()'},
angle: <>,
@@ -154,7 +165,7 @@ export const Kv: FunctionComponent = () => {
<>
@@ -166,8 +177,7 @@ export const Kv: FunctionComponent = () => {
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText',
{
- defaultMessage:
- 'Field to insert the extracted keys into. Defaults to the root of the document.',
+ defaultMessage: 'Output field for the extracted fields. Defaults to the document root.',
}
)}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
index 9db313a05007f..0d8170338ea10 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FieldNameField } from './common_fields/field_name_field';
import { TargetField } from './common_fields/target_field';
@@ -23,17 +21,7 @@ export const Lowercase: FunctionComponent = () => {
)}
/>
- {'field'},
- }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
index c785cf935833d..57843e2411359 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
@@ -27,7 +27,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText',
{
- defaultMessage: 'Name of the pipeline to execute.',
+ defaultMessage: 'Name of the ingest pipeline to run.',
}
),
validations: [
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
index 3e90ce2b76f7b..3ba1cdb0c802d 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
@@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Fields',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', {
- defaultMessage: 'Fields to be removed.',
+ defaultMessage: 'Fields to remove.',
}),
validations: [
{
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
index 8b796d9664586..099e2bd2c80fb 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
@@ -21,7 +21,7 @@ export const Rename: FunctionComponent = () => {
@@ -31,7 +31,7 @@ export const Rename: FunctionComponent = () => {
})}
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText',
- { defaultMessage: 'Name of the new field.' }
+ { defaultMessage: 'New field name. This field cannot already exist.' }
)}
validations={[
{
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
index ae0bbbb490ae9..de28f66766603 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
@@ -32,7 +32,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText',
{
- defaultMessage: 'Stored script reference.',
+ defaultMessage: 'ID of the stored script to run.',
}
),
validations: [
@@ -55,7 +55,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText',
{
- defaultMessage: 'Script to be executed.',
+ defaultMessage: 'Inline script to run.',
}
),
validations: [
@@ -98,7 +98,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText',
{
- defaultMessage: 'Script parameters.',
+ defaultMessage: 'Named parameters passed to the script as variables.',
}
),
validations: [
@@ -128,7 +128,7 @@ export const Script: FormFieldsComponent = ({ initialFieldValues }) => {
setShowId((v) => !v)}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
index c282be35e5071..04ea0c44c3513 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
@@ -32,13 +32,13 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Value',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', {
- defaultMessage: 'Value to be set for the field',
+ defaultMessage: 'Value for the field.',
}),
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', {
- defaultMessage: 'A value is required',
+ defaultMessage: 'A value is required.',
})
),
},
@@ -53,9 +53,15 @@ const fieldsConfig: FieldsConfig = {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', {
defaultMessage: 'Override',
}),
- helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', {
- defaultMessage: 'If disabled, fields containing non-null values will not be updated.',
- }),
+ helpText: (
+ {'null'},
+ }}
+ />
+ ),
},
ignore_empty_value: {
type: FIELD_TYPES.TOGGLE,
@@ -71,7 +77,8 @@ const fieldsConfig: FieldsConfig = {
helpText: (
{'value'},
nullValue: {'null'},
@@ -89,7 +96,7 @@ export const SetProcessor: FunctionComponent = () => {
<>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
index 78128b3d54c75..46bfe8c97ebea 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
@@ -44,7 +44,7 @@ const fieldsConfig: FieldsConfig = {
helpText: (
[{helpTextValues}],
}}
@@ -60,7 +60,7 @@ export const SetSecurityUser: FunctionComponent = () => {
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField',
{
- defaultMessage: 'Field to store the user information',
+ defaultMessage: 'Output field.',
}
)}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
index cdd0ff888accf..c8c0562011fd6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
@@ -24,7 +24,8 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Order',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', {
- defaultMessage: 'Sort order to use',
+ defaultMessage:
+ 'Sort order. Arrays containing a mix of strings and numbers are sorted lexicographically.',
}),
},
};
@@ -35,7 +36,7 @@ export const Sort: FunctionComponent = () => {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
index b48ce74110b39..fa178aaddd314 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
@@ -33,7 +33,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText',
{
- defaultMessage: 'Regex to match a separator',
+ defaultMessage: 'Regex pattern used to delimit the field value.',
}
),
validations: [
@@ -60,7 +60,7 @@ const fieldsConfig: FieldsConfig = {
),
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText',
- { defaultMessage: 'If enabled, preserve any trailing space.' }
+ { defaultMessage: 'Preserve any trailing whitespace in the split field values.' }
),
},
};
@@ -71,7 +71,7 @@ export const Split: FunctionComponent = () => {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
index 799551b296bab..9de371f8d0024 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
@@ -7,7 +7,8 @@
import { i18n } from '@kbn/i18n';
import React, { ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
+import { EuiCode, EuiLink } from '@elastic/eui';
+import { useKibana } from '../../../../../shared_imports';
import {
Append,
@@ -106,7 +107,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
defaultMessage: 'CSV',
}),
description: i18n.translate('xpack.ingestPipelines.processors.description.csv', {
- defaultMessage: 'Extracts fields values from CSV data.',
+ defaultMessage: 'Extracts field values from CSV data.',
}),
},
date: {
@@ -171,6 +172,25 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.enrich', {
defaultMessage: 'Enrich',
}),
+ description: function Description() {
+ const {
+ services: { documentation },
+ } = useKibana();
+ const esDocUrl = documentation.getEsDocsBasePath();
+ return (
+
+ {'enrich policy'}
+
+ ),
+ }}
+ />
+ );
+ },
},
fail: {
FieldsComponent: Fail,
@@ -178,6 +198,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.fail', {
defaultMessage: 'Fail',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.fail', {
+ defaultMessage:
+ 'Returns a custom error message on failure. Often used to notify requesters of required conditions.',
+ }),
},
foreach: {
FieldsComponent: Foreach,
@@ -185,6 +209,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.foreach', {
defaultMessage: 'Foreach',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.foreach', {
+ defaultMessage: 'Applies an ingest processor to each value in an array.',
+ }),
},
geoip: {
FieldsComponent: GeoIP,
@@ -192,6 +219,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.geoip', {
defaultMessage: 'GeoIP',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.geoip', {
+ defaultMessage:
+ 'Adds geo data based on an IP address. Uses geo data from a Maxmind database file.',
+ }),
},
grok: {
FieldsComponent: Grok,
@@ -199,6 +230,25 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.grok', {
defaultMessage: 'Grok',
}),
+ description: function Description() {
+ const {
+ services: { documentation },
+ } = useKibana();
+ const esDocUrl = documentation.getEsDocsBasePath();
+ return (
+
+ {'grok'}
+
+ ),
+ }}
+ />
+ );
+ },
},
gsub: {
FieldsComponent: Gsub,
@@ -206,6 +256,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.gsub', {
defaultMessage: 'Gsub',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.gsub', {
+ defaultMessage: 'Uses a regular expression to replace field substrings.',
+ }),
},
html_strip: {
FieldsComponent: HtmlStrip,
@@ -213,6 +266,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.htmlStrip', {
defaultMessage: 'HTML strip',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.htmlStrip', {
+ defaultMessage: 'Removes HTML tags from a field.',
+ }),
},
inference: {
FieldsComponent: Inference,
@@ -220,6 +276,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.inference', {
defaultMessage: 'Inference',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.inference', {
+ defaultMessage:
+ 'Uses a pre-trained data frame analytics model to infer against incoming data.',
+ }),
},
join: {
FieldsComponent: Join,
@@ -227,6 +287,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.join', {
defaultMessage: 'Join',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.join', {
+ defaultMessage:
+ 'Joins array elements into a string. Inserts a separator between each element.',
+ }),
},
json: {
FieldsComponent: Json,
@@ -234,12 +298,18 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.json', {
defaultMessage: 'JSON',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.json', {
+ defaultMessage: 'Creates a JSON object from a compatible string.',
+ }),
},
kv: {
FieldsComponent: Kv,
docLinkPath: '/kv-processor.html',
label: i18n.translate('xpack.ingestPipelines.processors.label.kv', {
- defaultMessage: 'KV',
+ defaultMessage: 'Key-value (KV)',
+ }),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.kv', {
+ defaultMessage: 'Extracts fields from a string containing key-value pairs.',
}),
},
lowercase: {
@@ -248,6 +318,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', {
defaultMessage: 'Lowercase',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.lowercase', {
+ defaultMessage: 'Converts a string to lowercase.',
+ }),
},
pipeline: {
FieldsComponent: Pipeline,
@@ -255,6 +328,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', {
defaultMessage: 'Pipeline',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.pipeline', {
+ defaultMessage: 'Runs another ingest node pipeline.',
+ }),
},
remove: {
FieldsComponent: Remove,
@@ -262,6 +338,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.remove', {
defaultMessage: 'Remove',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.remove', {
+ defaultMessage: 'Removes one or more fields.',
+ }),
},
rename: {
FieldsComponent: Rename,
@@ -269,6 +348,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.rename', {
defaultMessage: 'Rename',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.rename', {
+ defaultMessage: 'Renames an existing field.',
+ }),
},
script: {
FieldsComponent: Script,
@@ -276,6 +358,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.script', {
defaultMessage: 'Script',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.script', {
+ defaultMessage: 'Runs a script on incoming documents.',
+ }),
},
set: {
FieldsComponent: SetProcessor,
@@ -283,6 +368,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.set', {
defaultMessage: 'Set',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.set', {
+ defaultMessage: 'Sets the value of a field.',
+ }),
},
set_security_user: {
FieldsComponent: SetSecurityUser,
@@ -290,12 +378,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', {
defaultMessage: 'Set security user',
}),
- },
- split: {
- FieldsComponent: Split,
- docLinkPath: '/split-processor.html',
- label: i18n.translate('xpack.ingestPipelines.processors.label.split', {
- defaultMessage: 'Split',
+ description: i18n.translate('xpack.ingestPipelines.processors.description.setSecurityUser', {
+ defaultMessage:
+ 'Adds details about the current user, such user name and email address, to incoming documents. Requires an authenticated user for the indexing request.',
}),
},
sort: {
@@ -304,6 +389,19 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.sort', {
defaultMessage: 'Sort',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.sort', {
+ defaultMessage: "Sorts a field's array elements.",
+ }),
+ },
+ split: {
+ FieldsComponent: Split,
+ docLinkPath: '/split-processor.html',
+ label: i18n.translate('xpack.ingestPipelines.processors.label.split', {
+ defaultMessage: 'Split',
+ }),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.split', {
+ defaultMessage: 'Splits a field value into an array.',
+ }),
},
trim: {
FieldsComponent: undefined, // TODO: Implement
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
index 198be7085f5fc..e5d63f1f92e19 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
@@ -80,7 +80,8 @@ export function BucketNestingEditor({
values: { field: fieldName },
})
: i18n.translate('xpack.lens.indexPattern.groupingOverallDateHistogram', {
- defaultMessage: 'Dates overall',
+ defaultMessage: 'Top values for each {field}',
+ values: { field: fieldName },
})
}
checked={!prevColumn}
@@ -96,7 +97,7 @@ export function BucketNestingEditor({
values: { target: target.fieldName },
})
: i18n.translate('xpack.lens.indexPattern.groupingSecondDateHistogram', {
- defaultMessage: 'Dates for each {target}',
+ defaultMessage: 'Overall top {target}',
values: { target: target.fieldName },
})
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index a0cc5ec352130..cf15c29844053 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -117,14 +117,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
);
function fetchData() {
- if (
- state.isLoading ||
- (field.type !== 'number' &&
- field.type !== 'string' &&
- field.type !== 'date' &&
- field.type !== 'boolean' &&
- field.type !== 'ip')
- ) {
+ if (state.isLoading) {
return;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index 660be9514a92f..19213d4afc9bc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -93,6 +93,16 @@ const indexPattern1 = ({
searchable: true,
esTypes: ['keyword'],
},
+ {
+ name: 'scripted',
+ displayName: 'Scripted',
+ type: 'string',
+ searchable: true,
+ aggregatable: true,
+ scripted: true,
+ lang: 'painless',
+ script: '1234',
+ },
documentField,
],
} as unknown) as IndexPattern;
@@ -156,12 +166,13 @@ const indexPattern2 = ({
aggregatable: true,
searchable: true,
scripted: true,
+ lang: 'painless',
+ script: '1234',
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
- esTypes: ['keyword'],
},
documentField,
],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
index 585a1281cbf51..0ab658b961336 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
@@ -55,15 +55,27 @@ export async function loadIndexPatterns({
!indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted)
)
.map(
- (field): IndexPatternField => ({
- name: field.name,
- displayName: field.displayName,
- type: field.type,
- aggregatable: field.aggregatable,
- searchable: field.searchable,
- scripted: field.scripted,
- esTypes: field.esTypes,
- })
+ (field): IndexPatternField => {
+ // Convert the getters on the index pattern service into plain JSON
+ const base = {
+ name: field.name,
+ displayName: field.displayName,
+ type: field.type,
+ aggregatable: field.aggregatable,
+ searchable: field.searchable,
+ esTypes: field.esTypes,
+ scripted: field.scripted,
+ };
+
+ // Simplifies tests by hiding optional properties instead of undefined
+ return base.scripted
+ ? {
+ ...base,
+ lang: field.lang,
+ script: field.script,
+ }
+ : base;
+ }
)
.concat(documentField);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
index 31e6240993d36..21ed23321cf57 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
@@ -64,6 +64,16 @@ export const createMockedIndexPattern = (): IndexPattern => ({
searchable: true,
esTypes: ['keyword'],
},
+ {
+ name: 'scripted',
+ displayName: 'Scripted',
+ type: 'string',
+ searchable: true,
+ aggregatable: true,
+ scripted: true,
+ lang: 'painless',
+ script: '1234',
+ },
],
});
@@ -95,6 +105,8 @@ export const createMockedRestrictedIndexPattern = () => ({
searchable: true,
scripted: true,
esTypes: ['keyword'],
+ lang: 'painless',
+ script: '1234',
},
],
typeMeta: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
index c101f1354b703..21ca41234fdf1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IFieldType } from 'src/plugins/data/common';
import { IndexPatternColumn } from './operations';
import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public';
@@ -22,16 +23,10 @@ export interface IndexPattern {
hasRestrictions: boolean;
}
-export interface IndexPatternField {
- name: string;
+export type IndexPatternField = IFieldType & {
displayName: string;
- type: string;
- esTypes?: string[];
- aggregatable: boolean;
- scripted?: boolean;
- searchable: boolean;
aggregationRestrictions?: Partial;
-}
+};
export interface IndexPatternLayer {
columnOrder: string[];
diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts
index 20d3e2b4164ca..a7368a12f0e2c 100644
--- a/x-pack/plugins/lens/server/routes/field_stats.ts
+++ b/x-pack/plugins/lens/server/routes/field_stats.ts
@@ -8,6 +8,7 @@ import Boom from 'boom';
import DateMath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
import { CoreSetup } from 'src/core/server';
+import { IFieldType } from 'src/plugins/data/common';
import { ESSearchResponse } from '../../../apm/typings/elasticsearch';
import { FieldStatsResponse, BASE_API_URL } from '../../common';
@@ -33,6 +34,9 @@ export async function initFieldsRoute(setup: CoreSetup) {
name: schema.string(),
type: schema.string(),
esTypes: schema.maybe(schema.arrayOf(schema.string())),
+ scripted: schema.maybe(schema.boolean()),
+ lang: schema.maybe(schema.string()),
+ script: schema.maybe(schema.string()),
},
{ unknowns: 'allow' }
),
@@ -83,21 +87,15 @@ export async function initFieldsRoute(setup: CoreSetup) {
return res.ok({
body: await getNumberHistogram(search, field),
});
- } else if (field.type === 'string') {
- return res.ok({
- body: await getStringSamples(search, field),
- });
} else if (field.type === 'date') {
return res.ok({
body: await getDateHistogram(search, field, { fromDate, toDate }),
});
- } else if (field.type === 'boolean') {
- return res.ok({
- body: await getStringSamples(search, field),
- });
}
- return res.ok({});
+ return res.ok({
+ body: await getStringSamples(search, field),
+ });
} catch (e) {
if (e.status === 404) {
return res.notFound();
@@ -119,8 +117,10 @@ export async function initFieldsRoute(setup: CoreSetup) {
export async function getNumberHistogram(
aggSearchWithBody: (body: unknown) => Promise,
- field: { name: string; type: string; esTypes?: string[] }
+ field: IFieldType
): Promise {
+ const fieldRef = getFieldRef(field);
+
const searchBody = {
sample: {
sampler: { shard_size: SHARD_SIZE },
@@ -131,9 +131,9 @@ export async function getNumberHistogram(
max_value: {
max: { field: field.name },
},
- sample_count: { value_count: { field: field.name } },
+ sample_count: { value_count: { ...fieldRef } },
top_values: {
- terms: { field: field.name, size: 10 },
+ terms: { ...fieldRef, size: 10 },
},
},
},
@@ -206,15 +206,20 @@ export async function getNumberHistogram(
export async function getStringSamples(
aggSearchWithBody: (body: unknown) => unknown,
- field: { name: string; type: string }
+ field: IFieldType
): Promise {
+ const fieldRef = getFieldRef(field);
+
const topValuesBody = {
sample: {
sampler: { shard_size: SHARD_SIZE },
aggs: {
- sample_count: { value_count: { field: field.name } },
+ sample_count: { value_count: { ...fieldRef } },
top_values: {
- terms: { field: field.name, size: 10 },
+ terms: {
+ ...fieldRef,
+ size: 10,
+ },
},
},
},
@@ -241,7 +246,7 @@ export async function getStringSamples(
// This one is not sampled so that it returns the full date range
export async function getDateHistogram(
aggSearchWithBody: (body: unknown) => unknown,
- field: { name: string; type: string },
+ field: IFieldType,
range: { fromDate: string; toDate: string }
): Promise {
const fromDate = DateMath.parse(range.fromDate);
@@ -265,7 +270,7 @@ export async function getDateHistogram(
const fixedInterval = `${interval}ms`;
const histogramBody = {
- histo: { date_histogram: { field: field.name, fixed_interval: fixedInterval } },
+ histo: { date_histogram: { ...getFieldRef(field), fixed_interval: fixedInterval } },
};
const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse<
unknown,
@@ -283,3 +288,14 @@ export async function getDateHistogram(
},
};
}
+
+function getFieldRef(field: IFieldType) {
+ return field.scripted
+ ? {
+ script: {
+ lang: field.lang as string,
+ source: field.script as string,
+ },
+ }
+ : { field: field.name };
+}
diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss
index 76ce9f1bc79e3..726573ce4307d 100644
--- a/x-pack/plugins/maps/public/components/_index.scss
+++ b/x-pack/plugins/maps/public/components/_index.scss
@@ -1,4 +1,4 @@
@import 'action_select';
-@import 'metric_editors';
+@import 'metrics_editor/metric_editors';
@import './geometry_filter';
@import 'tooltip_selector/tooltip_selector';
diff --git a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap b/x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
similarity index 92%
rename from x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
rename to x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
index 0d4f1f99e464c..bd58ded41e7f5 100644
--- a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
+++ b/x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
@@ -16,8 +16,9 @@ exports[`should add default count metric when metrics is empty array 1`] = `
"type": "count",
}
}
- metricsFilter={[Function]}
onChange={[Function]}
+ onRemove={[Function]}
+ showRemoveButton={false}
/>
@@ -59,8 +60,9 @@ exports[`should render metrics editor 1`] = `
"type": "sum",
}
}
- metricsFilter={[Function]}
onChange={[Function]}
+ onRemove={[Function]}
+ showRemoveButton={false}
/>
diff --git a/x-pack/plugins/maps/public/components/_metric_editors.scss b/x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss
similarity index 100%
rename from x-pack/plugins/maps/public/components/_metric_editors.scss
rename to x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss
diff --git a/x-pack/plugins/maps/public/components/metrics_editor/index.ts b/x-pack/plugins/maps/public/components/metrics_editor/index.ts
new file mode 100644
index 0000000000000..3c105c2d798ff
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/metrics_editor/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { MetricsEditor } from './metrics_editor';
diff --git a/x-pack/plugins/maps/public/components/metric_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
similarity index 59%
rename from x-pack/plugins/maps/public/components/metric_editor.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
index 96b52d84653b2..543d144efdcc7 100644
--- a/x-pack/plugins/maps/public/components/metric_editor.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
@@ -4,18 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { ChangeEvent, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFieldText, EuiFormRow } from '@elastic/eui';
+import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiFieldText, EuiFormRow } from '@elastic/eui';
-import { MetricSelect, METRIC_AGGREGATION_VALUES } from './metric_select';
-import { SingleFieldSelect } from './single_field_select';
-import { AGG_TYPE } from '../../common/constants';
-import { getTermsFields } from '../index_pattern_util';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MetricSelect } from './metric_select';
+import { SingleFieldSelect } from '../single_field_select';
+import { AggDescriptor } from '../../../common/descriptor_types';
+import { AGG_TYPE } from '../../../common/constants';
+import { getTermsFields } from '../../index_pattern_util';
+import { IFieldType } from '../../../../../../src/plugins/data/public';
-function filterFieldsForAgg(fields, aggType) {
+function filterFieldsForAgg(fields: IFieldType[], aggType: AGG_TYPE) {
if (!fields) {
return [];
}
@@ -34,8 +36,27 @@ function filterFieldsForAgg(fields, aggType) {
});
}
-export function MetricEditor({ fields, metricsFilter, metric, onChange, removeButton }) {
- const onAggChange = (metricAggregationType) => {
+interface Props {
+ metric: AggDescriptor;
+ fields: IFieldType[];
+ onChange: (metric: AggDescriptor) => void;
+ onRemove: () => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+ showRemoveButton: boolean;
+}
+
+export function MetricEditor({
+ fields,
+ metricsFilter,
+ metric,
+ onChange,
+ showRemoveButton,
+ onRemove,
+}: Props) {
+ const onAggChange = (metricAggregationType?: AGG_TYPE) => {
+ if (!metricAggregationType) {
+ return;
+ }
const newMetricProps = {
...metric,
type: metricAggregationType,
@@ -54,13 +75,16 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
onChange(newMetricProps);
};
- const onFieldChange = (fieldName) => {
+ const onFieldChange = (fieldName?: string) => {
+ if (!fieldName) {
+ return;
+ }
onChange({
...metric,
field: fieldName,
});
};
- const onLabelChange = (e) => {
+ const onLabelChange = (e: ChangeEvent) => {
onChange({
...metric,
label: e.target.value,
@@ -80,7 +104,7 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
placeholder={i18n.translate('xpack.maps.metricsEditor.selectFieldPlaceholder', {
defaultMessage: 'Select field',
})}
- value={metric.field}
+ value={metric.field ? metric.field : null}
onChange={onFieldChange}
fields={filterFieldsForAgg(fields, metric.type)}
isClearable={false}
@@ -108,6 +132,28 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
);
}
+ let removeButton;
+ if (showRemoveButton) {
+ removeButton = (
+
+
+
+
+
+ );
+ }
+
return (
);
}
-
-MetricEditor.propTypes = {
- metric: PropTypes.shape({
- type: PropTypes.oneOf(METRIC_AGGREGATION_VALUES),
- field: PropTypes.string,
- label: PropTypes.string,
- }),
- fields: PropTypes.array,
- onChange: PropTypes.func.isRequired,
- metricsFilter: PropTypes.func,
-};
diff --git a/x-pack/plugins/maps/public/components/metric_select.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
similarity index 80%
rename from x-pack/plugins/maps/public/components/metric_select.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
index 2ebfcf99dece6..197c5466fe0fd 100644
--- a/x-pack/plugins/maps/public/components/metric_select.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
@@ -5,10 +5,9 @@
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
-import { EuiComboBox } from '@elastic/eui';
-import { AGG_TYPE } from '../../common/constants';
+import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
+import { AGG_TYPE } from '../../../common/constants';
const AGG_OPTIONS = [
{
@@ -55,17 +54,19 @@ const AGG_OPTIONS = [
},
];
-export const METRIC_AGGREGATION_VALUES = AGG_OPTIONS.map(({ value }) => {
- return value;
-});
+type Props = Omit, 'onChange'> & {
+ value: AGG_TYPE;
+ onChange: (aggType: AGG_TYPE) => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+};
-export function MetricSelect({ value, onChange, metricsFilter, ...rest }) {
- function onAggChange(selectedOptions) {
+export function MetricSelect({ value, onChange, metricsFilter, ...rest }: Props) {
+ function onAggChange(selectedOptions: Array>) {
if (selectedOptions.length === 0) {
return;
}
- const aggType = selectedOptions[0].value;
+ const aggType = selectedOptions[0].value!;
onChange(aggType);
}
@@ -87,9 +88,3 @@ export function MetricSelect({ value, onChange, metricsFilter, ...rest }) {
/>
);
}
-
-MetricSelect.propTypes = {
- metricsFilter: PropTypes.func,
- value: PropTypes.oneOf(METRIC_AGGREGATION_VALUES),
- onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.test.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
similarity index 84%
rename from x-pack/plugins/maps/public/components/metrics_editor.test.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
index bcbeef29875ee..7ce7fbce2b066 100644
--- a/x-pack/plugins/maps/public/components/metrics_editor.test.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { MetricsEditor } from './metrics_editor';
-import { AGG_TYPE } from '../../common/constants';
+import { AGG_TYPE } from '../../../common/constants';
const defaultProps = {
metrics: [
@@ -19,15 +19,14 @@ const defaultProps = {
fields: [],
onChange: () => {},
allowMultipleMetrics: true,
- metricsFilter: () => {},
};
-test('should render metrics editor', async () => {
+test('should render metrics editor', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
-test('should add default count metric when metrics is empty array', async () => {
+test('should add default count metric when metrics is empty array', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
similarity index 54%
rename from x-pack/plugins/maps/public/components/metrics_editor.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
index 7d4d7bf3ec7ab..17cfc5f62fee5 100644
--- a/x-pack/plugins/maps/public/components/metrics_editor.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
@@ -5,48 +5,43 @@
*/
import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui';
+import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { MetricEditor } from './metric_editor';
-import { DEFAULT_METRIC } from '../classes/sources/es_agg_source';
+// @ts-expect-error
+import { DEFAULT_METRIC } from '../../classes/sources/es_agg_source';
+import { IFieldType } from '../../../../../../src/plugins/data/public';
+import { AggDescriptor } from '../../../common/descriptor_types';
+import { AGG_TYPE } from '../../../common/constants';
-export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) {
+interface Props {
+ allowMultipleMetrics: boolean;
+ metrics: AggDescriptor[];
+ fields: IFieldType[];
+ onChange: (metrics: AggDescriptor[]) => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+}
+
+export function MetricsEditor({
+ fields,
+ metrics = [DEFAULT_METRIC],
+ onChange,
+ allowMultipleMetrics = true,
+ metricsFilter,
+}: Props) {
function renderMetrics() {
// There was a bug in 7.8 that initialized metrics to [].
// This check is needed to handle any saved objects created before the bug was patched.
const nonEmptyMetrics = metrics.length === 0 ? [DEFAULT_METRIC] : metrics;
return nonEmptyMetrics.map((metric, index) => {
- const onMetricChange = (metric) => {
- onChange([...metrics.slice(0, index), metric, ...metrics.slice(index + 1)]);
+ const onMetricChange = (updatedMetric: AggDescriptor) => {
+ onChange([...metrics.slice(0, index), updatedMetric, ...metrics.slice(index + 1)]);
};
const onRemove = () => {
onChange([...metrics.slice(0, index), ...metrics.slice(index + 1)]);
};
- let removeButton;
- if (index > 0) {
- removeButton = (
-
-
-
-
-
- );
- }
return (
0}
+ onRemove={onRemove}
/>
);
@@ -62,7 +58,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
}
function addMetric() {
- onChange([...metrics, {}]);
+ onChange([...metrics, { type: AGG_TYPE.AVG }]);
}
function renderAddMetricButton() {
@@ -71,7 +67,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
}
return (
- <>
+
@@ -81,7 +77,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
/>
- >
+
);
}
@@ -93,16 +89,3 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
);
}
-
-MetricsEditor.propTypes = {
- metrics: PropTypes.array,
- fields: PropTypes.array,
- onChange: PropTypes.func.isRequired,
- allowMultipleMetrics: PropTypes.bool,
- metricsFilter: PropTypes.func,
-};
-
-MetricsEditor.defaultProps = {
- metrics: [DEFAULT_METRIC],
- allowMultipleMetrics: true,
-};
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
index 3cd8a3c42879a..e0e1556ecde06 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
@@ -4,12 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-jest.mock('../../../../components/metric_editor', () => ({
- MetricsEditor: () => {
- return mockMetricsEditor
;
- },
-}));
-
import React from 'react';
import { shallow } from 'enzyme';
import { MetricsExpression } from './metrics_expression';
diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
index 5f2a640aa9d0f..03752a1c3e11e 100644
--- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
+++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
@@ -7,7 +7,7 @@
import { AnyAction } from 'redux';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IndexPatternsContract } from 'src/plugins/data/public/index_patterns';
-import { ReactElement } from 'react';
+import { AppMountContext, AppMountParameters } from 'kibana/public';
import { IndexPattern } from 'src/plugins/data/public';
import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public';
import { LayerDescriptor } from '../../common/descriptor_types';
@@ -40,7 +40,7 @@ interface LazyLoadedMapModules {
initialLayers?: LayerDescriptor[]
) => LayerDescriptor[];
mergeInputWithSavedMap: any;
- renderApp: (context: unknown, params: unknown) => ReactElement;
+ renderApp: (context: AppMountContext, params: AppMountParameters) => Promise<() => void>;
createSecurityLayerDescriptors: (
indexPatternId: string,
indexPatternTitle: string
@@ -57,7 +57,6 @@ export async function lazyLoadMapModules(): Promise {
loadModulesPromise = new Promise(async (resolve) => {
const {
- // @ts-expect-error
getMapsSavedObjectLoader,
getQueryableUniqueIndexPatternIds,
MapEmbeddable,
@@ -68,7 +67,6 @@ export async function lazyLoadMapModules(): Promise {
addLayerWithoutDataSync,
getInitialLayers,
mergeInputWithSavedMap,
- // @ts-expect-error
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
index e55160383a8f3..28f5acdc17656 100644
--- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
+++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
@@ -7,7 +7,6 @@
// These are map-dependencies of the embeddable.
// By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code.
-// @ts-expect-error
export * from '../../routing/bootstrap/services/gis_map_saved_object_loader';
export * from '../../embeddable/map_embeddable';
export * from '../../kibana_services';
@@ -16,7 +15,6 @@ export * from '../../actions';
export * from '../../selectors/map_selectors';
export * from '../../routing/bootstrap/get_initial_layers';
export * from '../../embeddable/merge_input_with_saved_map';
-// @ts-expect-error
export * from '../../routing/maps_router';
export * from '../../classes/layers/solution_layers/security';
export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry';
diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts
index b08135b4e486c..00ee7f376efc6 100644
--- a/x-pack/plugins/maps/public/plugin.ts
+++ b/x-pack/plugins/maps/public/plugin.ts
@@ -123,7 +123,6 @@ export class MapsPlugin
icon: `plugins/${APP_ID}/icon.svg`,
euiIconType: APP_ICON,
category: DEFAULT_APP_CATEGORIES.kibana,
- // @ts-expect-error
async mount(context, params) {
const { renderApp } = await lazyLoadMapModules();
return renderApp(context, params);
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
similarity index 87%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
index b47f83d5a6664..e828dc88409cb 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
@@ -15,15 +15,19 @@ import '../../classes/sources/es_pew_pew_source';
import '../../classes/sources/kibana_regionmap_source';
import '../../classes/sources/es_geo_grid_source';
import '../../classes/sources/xyz_tms_source';
+import { LayerDescriptor } from '../../../common/descriptor_types';
+// @ts-expect-error
import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source';
import { TileLayer } from '../../classes/layers/tile_layer/tile_layer';
+// @ts-expect-error
import { EMSTMSSource } from '../../classes/sources/ems_tms_source';
+// @ts-expect-error
import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer';
import { getIsEmsEnabled, getToasts } from '../../kibana_services';
import { INITIAL_LAYERS_KEY } from '../../../common/constants';
import { getKibanaTileMap } from '../../meta';
-export function getInitialLayers(layerListJSON, initialLayers = []) {
+export function getInitialLayers(layerListJSON?: string, initialLayers: LayerDescriptor[] = []) {
if (layerListJSON) {
return JSON.parse(layerListJSON);
}
@@ -58,9 +62,10 @@ export function getInitialLayersFromUrlParam() {
try {
let mapInitLayers = mapAppParams.get(INITIAL_LAYERS_KEY);
- if (mapInitLayers[mapInitLayers.length - 1] === '#') {
- mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1);
+ if (mapInitLayers![mapInitLayers!.length - 1] === '#') {
+ mapInitLayers = mapInitLayers!.substr(0, mapInitLayers!.length - 1);
}
+ // @ts-ignore
return rison.decode_array(mapInitLayers);
} catch (e) {
getToasts().addWarning({
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
similarity index 73%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
index 1f2cf27077623..43293d152dbff 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
@@ -5,8 +5,15 @@
*/
import { getData } from '../../kibana_services';
+import { MapsAppState } from '../state_syncing/app_state_manager';
-export function getInitialQuery({ mapStateJSON, appState = {} }) {
+export function getInitialQuery({
+ mapStateJSON,
+ appState = {},
+}: {
+ mapStateJSON?: string;
+ appState: MapsAppState;
+}) {
if (appState.query) {
return appState.query;
}
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
similarity index 81%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
index d7b3bbf5b4ab2..7d759cb25052f 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
@@ -4,10 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { QueryState } from 'src/plugins/data/public';
import { getUiSettings } from '../../kibana_services';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
-export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) {
+export function getInitialRefreshConfig({
+ mapStateJSON,
+ globalState = {},
+}: {
+ mapStateJSON?: string;
+ globalState: QueryState;
+}) {
const uiSettings = getUiSettings();
if (mapStateJSON) {
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
similarity index 75%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
index 9c11dabe03923..549cc154fe487 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
@@ -4,9 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { QueryState } from 'src/plugins/data/public';
import { getUiSettings } from '../../kibana_services';
-export function getInitialTimeFilters({ mapStateJSON, globalState }) {
+export function getInitialTimeFilters({
+ mapStateJSON,
+ globalState,
+}: {
+ mapStateJSON?: string;
+ globalState: QueryState;
+}) {
if (mapStateJSON) {
const mapState = JSON.parse(mapStateJSON);
if (mapState.timeFilters) {
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts
similarity index 100%
rename from x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js
rename to x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
index 6f8e7777f671b..511f015b0ff80 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
+++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
@@ -27,7 +27,6 @@ import { copyPersistentState } from '../../../reducers/util';
// @ts-expect-error
import { extractReferences, injectReferences } from '../../../../common/migrations/references';
import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants';
-// @ts-expect-error
import { getStore } from '../../store_operations';
import { MapStoreState } from '../../../reducers/store';
import { LayerDescriptor } from '../../../../common/descriptor_types';
diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.tsx
similarity index 80%
rename from x-pack/plugins/maps/public/routing/maps_router.js
rename to x-pack/plugins/maps/public/routing/maps_router.tsx
index f0f5234e3f989..5291d9c361161 100644
--- a/x-pack/plugins/maps/public/routing/maps_router.js
+++ b/x-pack/plugins/maps/public/routing/maps_router.tsx
@@ -6,8 +6,10 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
-import { Router, Switch, Route, Redirect } from 'react-router-dom';
+import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
+import { Provider } from 'react-redux';
+import { AppMountContext, AppMountParameters } from 'kibana/public';
import {
getCoreChrome,
getCoreI18n,
@@ -18,16 +20,19 @@ import {
import {
createKbnUrlStateStorage,
withNotifyOnErrors,
+ IKbnUrlStateStorage,
} from '../../../../../src/plugins/kibana_utils/public';
import { getStore } from './store_operations';
-import { Provider } from 'react-redux';
import { LoadListAndRender } from './routes/list/load_list_and_render';
import { LoadMapAndRender } from './routes/maps_app/load_map_and_render';
-export let goToSpecifiedPath;
-export let kbnUrlStateStorage;
+export let goToSpecifiedPath: (path: string) => void;
+export let kbnUrlStateStorage: IKbnUrlStateStorage;
-export async function renderApp(context, { appBasePath, element, history, onAppLeave }) {
+export async function renderApp(
+ context: AppMountContext,
+ { appBasePath, element, history, onAppLeave }: AppMountParameters
+) {
goToSpecifiedPath = (path) => history.push(path);
kbnUrlStateStorage = createKbnUrlStateStorage({
useHash: false,
@@ -42,11 +47,19 @@ export async function renderApp(context, { appBasePath, element, history, onAppL
};
}
-const App = ({ history, appBasePath, onAppLeave }) => {
+interface Props {
+ history: AppMountParameters['history'] | RouteComponentProps['history'];
+ appBasePath: AppMountParameters['appBasePath'];
+ onAppLeave: AppMountParameters['onAppLeave'];
+}
+
+const App: React.FC = ({ history, appBasePath, onAppLeave }) => {
const store = getStore();
const I18nContext = getCoreI18n().Context;
- const stateTransfer = getEmbeddableService()?.getStateTransfer(history);
+ const stateTransfer = getEmbeddableService()?.getStateTransfer(
+ history as AppMountParameters['history']
+ );
const { originatingApp } =
stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {};
@@ -66,7 +79,7 @@ const App = ({ history, appBasePath, onAppLeave }) => {
return (
-
+
getMapsSavedObjectLoader().find(search, this.state.listingLimit);
+ _find = (search: string) => getMapsSavedObjectLoader().find(search, this.state.listingLimit);
- _delete = (ids) => getMapsSavedObjectLoader().delete(ids);
+ _delete = (ids: string[]) => getMapsSavedObjectLoader().delete(ids);
debouncedFetch = _.debounce(async (filter) => {
const response = await this._find(filter);
@@ -135,10 +163,10 @@ export class MapsListView extends React.Component {
this.setState({ showDeleteModal: true });
};
- onTableChange = ({ page, sort = {} }) => {
+ onTableChange = ({ page, sort }: CriteriaWithPagination) => {
const { index: pageIndex, size: pageSize } = page;
- let { field: sortField, direction: sortDirection } = sort;
+ let { field: sortField, direction: sortDirection } = sort || {};
// 3rd sorting state that is not captured by sort - native order (no sort)
// when switching from desc to asc for the same field - use native order
@@ -147,8 +175,8 @@ export class MapsListView extends React.Component {
this.state.sortDirection === 'desc' &&
sortDirection === 'asc'
) {
- sortField = null;
- sortDirection = null;
+ sortField = undefined;
+ sortDirection = undefined;
}
this.setState({
@@ -165,8 +193,8 @@ export class MapsListView extends React.Component {
if (this.state.sortField) {
itemsCopy.sort((a, b) => {
- const fieldA = _.get(a, this.state.sortField, '');
- const fieldB = _.get(b, this.state.sortField, '');
+ const fieldA = _.get(a, this.state.sortField!, '');
+ const fieldB = _.get(b, this.state.sortField!, '');
let order = 1;
if (this.state.sortDirection === 'desc') {
order = -1;
@@ -320,7 +348,7 @@ export class MapsListView extends React.Component {
}
renderTable() {
- const tableColumns = [
+ const tableColumns: Array> = [
{
field: 'title',
name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', {
@@ -329,7 +357,7 @@ export class MapsListView extends React.Component {
sortable: true,
render: (field, record) => (
{
+ onClick={(e: MouseEvent) => {
e.preventDefault();
goToSpecifiedPath(`/map/${record.id}`);
}}
@@ -355,12 +383,12 @@ export class MapsListView extends React.Component {
pageSizeOptions: [10, 20, 50],
};
- let selection = false;
+ let selection;
if (!this.state.readOnly) {
selection = {
- onSelectionChange: (selection) => {
+ onSelectionChange: (s: SelectionItem[]) => {
this.setState({
- selectedIds: selection.map((item) => {
+ selectedIds: s.map((item) => {
return item.id;
}),
});
@@ -368,11 +396,11 @@ export class MapsListView extends React.Component {
};
}
- const sorting = {};
+ const sorting: EuiTableSortingType = {};
if (this.state.sortField) {
sorting.sort = {
field: this.state.sortField,
- direction: this.state.sortDirection,
+ direction: this.state.sortDirection!,
};
}
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
index 1ccf890597edc..149c04b414c18 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
@@ -6,7 +6,6 @@
import { i18n } from '@kbn/i18n';
import { getNavigateToApp } from '../../../kibana_services';
-// @ts-expect-error
import { goToSpecifiedPath } from '../../maps_router';
export const unsavedChangesWarning = i18n.translate(
@@ -25,7 +24,7 @@ export function getBreadcrumbs({
title: string;
getHasUnsavedChanges: () => boolean;
originatingApp?: string;
- getAppNameFromId?: (id: string) => string;
+ getAppNameFromId?: (id: string) => string | undefined;
}) {
const breadcrumbs = [];
if (originatingApp && getAppNameFromId) {
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
similarity index 62%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/index.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
index 326db7289e60d..812d7fcf30981 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
@@ -5,6 +5,9 @@
*/
import { connect } from 'react-redux';
+import { ThunkDispatch } from 'redux-thunk';
+import { AnyAction } from 'redux';
+import { Filter, Query, TimeRange } from 'src/plugins/data/public';
import { MapsAppView } from './maps_app_view';
import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors';
import {
@@ -33,8 +36,15 @@ import {
import { FLYOUT_STATE } from '../../../reducers/ui';
import { getMapsCapabilities } from '../../../kibana_services';
import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
+import { MapStoreState } from '../../../reducers/store';
+import {
+ MapRefreshConfig,
+ MapCenterAndZoom,
+ LayerDescriptor,
+} from '../../../../common/descriptor_types';
+import { MapSettings } from '../../../reducers/map';
-function mapStateToProps(state = {}) {
+function mapStateToProps(state: MapStoreState) {
return {
isFullScreen: getIsFullScreen(state),
isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE,
@@ -50,9 +60,19 @@ function mapStateToProps(state = {}) {
};
}
-function mapDispatchToProps(dispatch) {
+function mapDispatchToProps(dispatch: ThunkDispatch) {
return {
- dispatchSetQuery: ({ forceRefresh, filters, query, timeFilters }) => {
+ dispatchSetQuery: ({
+ forceRefresh,
+ filters,
+ query,
+ timeFilters,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ timeFilters?: TimeRange;
+ forceRefresh?: boolean;
+ }) => {
dispatch(
setQuery({
filters,
@@ -62,12 +82,13 @@ function mapDispatchToProps(dispatch) {
})
);
},
- setRefreshConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)),
- replaceLayerList: (layerList) => dispatch(replaceLayerList(layerList)),
- setGotoWithCenter: (latLonZoom) => dispatch(setGotoWithCenter(latLonZoom)),
- setMapSettings: (mapSettings) => dispatch(setMapSettings(mapSettings)),
- setIsLayerTOCOpen: (isLayerTOCOpen) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)),
- setOpenTOCDetails: (openTOCDetails) => dispatch(setOpenTOCDetails(openTOCDetails)),
+ setRefreshConfig: (refreshConfig: MapRefreshConfig) =>
+ dispatch(setRefreshConfig(refreshConfig)),
+ replaceLayerList: (layerList: LayerDescriptor[]) => dispatch(replaceLayerList(layerList)),
+ setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => dispatch(setGotoWithCenter(latLonZoom)),
+ setMapSettings: (mapSettings: MapSettings) => dispatch(setMapSettings(mapSettings)),
+ setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)),
+ setOpenTOCDetails: (openTOCDetails: string[]) => dispatch(setOpenTOCDetails(openTOCDetails)),
clearUi: () => {
dispatch(setSelectedLayer(null));
dispatch(updateFlyout(FLYOUT_STATE.NONE));
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
similarity index 75%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
index eebbb17582821..7ab138300dc4c 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
@@ -5,15 +5,31 @@
*/
import React from 'react';
-import { MapsAppView } from '.';
-import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader';
-import { getCoreChrome, getToasts } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { Redirect } from 'react-router-dom';
+import { AppMountParameters } from 'kibana/public';
+import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
+import { getCoreChrome, getToasts } from '../../../kibana_services';
+import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader';
+import { MapsAppView } from '.';
+import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
+
+interface Props {
+ savedMapId?: string;
+ onAppLeave: AppMountParameters['onAppLeave'];
+ stateTransfer: EmbeddableStateTransfer;
+ originatingApp?: string;
+}
+
+interface State {
+ savedMap?: ISavedGisMap;
+ failedToLoad: boolean;
+}
-export const LoadMapAndRender = class extends React.Component {
- state = {
- savedMap: null,
+export const LoadMapAndRender = class extends React.Component {
+ _isMounted: boolean = false;
+ state: State = {
+ savedMap: undefined,
failedToLoad: false,
};
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
similarity index 73%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
index 485b0ed7682fa..b3377547b2dd1 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
@@ -7,6 +7,9 @@
import React from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
import _ from 'lodash';
+import { AppLeaveAction, AppMountParameters } from 'kibana/public';
+import { EmbeddableStateTransfer, Adapters } from 'src/plugins/embeddable/public';
+import { Subscription } from 'rxjs';
import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui';
import {
getData,
@@ -23,29 +26,91 @@ import {
getGlobalState,
updateGlobalState,
startGlobalStateSyncing,
+ MapsGlobalState,
} from '../../state_syncing/global_sync';
import { AppStateManager } from '../../state_syncing/app_state_manager';
import { startAppStateSyncing } from '../../state_syncing/app_sync';
-import { esFilters } from '../../../../../../../src/plugins/data/public';
+import {
+ esFilters,
+ Filter,
+ Query,
+ TimeRange,
+ IndexPattern,
+ SavedQuery,
+ QueryStateChange,
+ QueryState,
+} from '../../../../../../../src/plugins/data/public';
import { MapContainer } from '../../../connected_components/map_container';
import { getIndexPatternsFromIds } from '../../../index_pattern_util';
import { getTopNavConfig } from './top_nav_config';
import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs';
+import {
+ LayerDescriptor,
+ MapRefreshConfig,
+ MapCenterAndZoom,
+ MapQuery,
+} from '../../../../common/descriptor_types';
+import { MapSettings } from '../../../reducers/map';
+import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
+
+interface Props {
+ savedMap: ISavedGisMap;
+ onAppLeave: AppMountParameters['onAppLeave'];
+ stateTransfer: EmbeddableStateTransfer;
+ originatingApp?: string;
+ layerListConfigOnly: LayerDescriptor[];
+ replaceLayerList: (layerList: LayerDescriptor[]) => void;
+ filters: Filter[];
+ isFullScreen: boolean;
+ isOpenSettingsDisabled: boolean;
+ enableFullScreen: () => void;
+ openMapSettings: () => void;
+ inspectorAdapters: Adapters;
+ nextIndexPatternIds: string[];
+ dispatchSetQuery: ({
+ forceRefresh,
+ filters,
+ query,
+ timeFilters,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ timeFilters?: TimeRange;
+ forceRefresh?: boolean;
+ }) => void;
+ timeFilters: TimeRange;
+ refreshConfig: MapRefreshConfig;
+ setRefreshConfig: (refreshConfig: MapRefreshConfig) => void;
+ isSaveDisabled: boolean;
+ clearUi: () => void;
+ setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => void;
+ setMapSettings: (mapSettings: MapSettings) => void;
+ setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => void;
+ setOpenTOCDetails: (openTOCDetails: string[]) => void;
+ query: MapQuery | undefined;
+}
-export class MapsAppView extends React.Component {
- _globalSyncUnsubscribe = null;
- _globalSyncChangeMonitorSubscription = null;
- _appSyncUnsubscribe = null;
+interface State {
+ initialized: boolean;
+ initialLayerListConfig?: LayerDescriptor[];
+ indexPatterns: IndexPattern[];
+ savedQuery?: SavedQuery;
+ originatingApp?: string;
+}
+
+export class MapsAppView extends React.Component {
+ _globalSyncUnsubscribe: (() => void) | null = null;
+ _globalSyncChangeMonitorSubscription: Subscription | null = null;
+ _appSyncUnsubscribe: (() => void) | null = null;
_appStateManager = new AppStateManager();
- _prevIndexPatternIds = null;
+ _prevIndexPatternIds: string[] | null = null;
+ _isMounted: boolean = false;
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = {
indexPatterns: [],
initialized: false,
- savedQuery: '',
- initialLayerListConfig: null,
// tracking originatingApp in state so the connection can be broken by users
originatingApp: props.originatingApp,
};
@@ -60,10 +125,11 @@ export class MapsAppView extends React.Component {
this._updateFromGlobalState
);
- const initialSavedQuery = this._appStateManager.getAppState().savedQuery;
- if (initialSavedQuery) {
- this._updateStateFromSavedQuery(initialSavedQuery);
- }
+ // savedQuery must be fetched from savedQueryId
+ // const initialSavedQuery = this._appStateManager.getAppState().savedQuery;
+ // if (initialSavedQuery) {
+ // this._updateStateFromSavedQuery(initialSavedQuery as SavedQuery);
+ // }
this._initMap();
@@ -72,10 +138,10 @@ export class MapsAppView extends React.Component {
this.props.onAppLeave((actions) => {
if (this._hasUnsavedChanges()) {
if (!window.confirm(unsavedChangesWarning)) {
- return;
+ return {} as AppLeaveAction;
}
}
- return actions.default();
+ return actions.default() as AppLeaveAction;
});
}
@@ -121,7 +187,13 @@ export class MapsAppView extends React.Component {
getCoreChrome().setBreadcrumbs(breadcrumbs);
};
- _updateFromGlobalState = ({ changes, state: globalState }) => {
+ _updateFromGlobalState = ({
+ changes,
+ state: globalState,
+ }: {
+ changes: QueryStateChange;
+ state: QueryState;
+ }) => {
if (!this.state.initialized || !changes || !globalState) {
return;
}
@@ -144,7 +216,17 @@ export class MapsAppView extends React.Component {
}
}
- _onQueryChange = ({ filters, query, time, forceRefresh = false }) => {
+ _onQueryChange = ({
+ filters,
+ query,
+ time,
+ forceRefresh = false,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ time?: TimeRange;
+ forceRefresh?: boolean;
+ }) => {
const { filterManager } = getData().query;
if (filters) {
@@ -165,7 +247,9 @@ export class MapsAppView extends React.Component {
});
// sync globalState
- const updatedGlobalState = { filters: filterManager.getGlobalFilters() };
+ const updatedGlobalState: MapsGlobalState = {
+ filters: filterManager.getGlobalFilters(),
+ };
if (time) {
updatedGlobalState.time = time;
}
@@ -173,7 +257,7 @@ export class MapsAppView extends React.Component {
};
_initMapAndLayerSettings() {
- const globalState = getGlobalState();
+ const globalState: MapsGlobalState = getGlobalState();
const mapStateJSON = this.props.savedMap.mapStateJSON;
let savedObjectFilters = [];
@@ -219,14 +303,14 @@ export class MapsAppView extends React.Component {
});
}
- _onFiltersChange = (filters) => {
+ _onFiltersChange = (filters: Filter[]) => {
this._onQueryChange({
filters,
});
};
// mapRefreshConfig: MapRefreshConfig
- _onRefreshConfigChange(mapRefreshConfig) {
+ _onRefreshConfigChange(mapRefreshConfig: MapRefreshConfig) {
this.props.setRefreshConfig(mapRefreshConfig);
updateGlobalState(
{
@@ -239,9 +323,9 @@ export class MapsAppView extends React.Component {
);
}
- _updateStateFromSavedQuery = (savedQuery) => {
+ _updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
this.setState({ savedQuery: { ...savedQuery } });
- this._appStateManager.setQueryAndFilters({ savedQuery });
+ this._appStateManager.setQueryAndFilters({ savedQueryId: savedQuery.id });
const { filterManager } = getData().query;
const savedQueryFilters = savedQuery.attributes.filters || [];
@@ -328,7 +412,13 @@ export class MapsAppView extends React.Component {
dateRangeTo={this.props.timeFilters.to}
isRefreshPaused={this.props.refreshConfig.isPaused}
refreshInterval={this.props.refreshConfig.interval}
- onRefreshChange={({ isPaused, refreshInterval }) => {
+ onRefreshChange={({
+ isPaused,
+ refreshInterval,
+ }: {
+ isPaused: boolean;
+ refreshInterval: number;
+ }) => {
this._onRefreshConfigChange({
isPaused,
interval: refreshInterval,
@@ -337,14 +427,14 @@ export class MapsAppView extends React.Component {
showSearchBar={true}
showFilterBar={true}
showDatePicker={true}
- showSaveQuery={getMapsCapabilities().saveQuery}
+ showSaveQuery={!!getMapsCapabilities().saveQuery}
savedQuery={this.state.savedQuery}
onSaved={this._updateStateFromSavedQuery}
onSavedQueryUpdated={this._updateStateFromSavedQuery}
onClearSavedQuery={() => {
const { filterManager, queryString } = getData().query;
- this.setState({ savedQuery: '' });
- this._appStateManager.setQueryAndFilters({ savedQuery: '' });
+ this.setState({ savedQuery: undefined });
+ this._appStateManager.setQueryAndFilters({ savedQueryId: '' });
this._onQueryChange({
filters: filterManager.getGlobalFilters(),
query: queryString.getDefaultQuery(),
@@ -354,7 +444,7 @@ export class MapsAppView extends React.Component {
);
}
- _addFilter = (newFilters) => {
+ _addFilter = async (newFilters: Filter[]) => {
newFilters.forEach((filter) => {
filter.$state = { store: esFilters.FilterStateStore.APP_STATE };
});
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
index 497c87ad533a6..47f41f2b76f3e 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
@@ -21,7 +21,6 @@ import {
showSaveModal,
} from '../../../../../../../src/plugins/saved_objects/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants';
-// @ts-expect-error
import { goToSpecifiedPath } from '../../maps_router';
import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
import { EmbeddableStateTransfer } from '../../../../../../../src/plugins/embeddable/public';
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
similarity index 58%
rename from x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js
rename to x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
index 4cdba13bd85d2..122b50f823a95 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js
+++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
@@ -5,20 +5,27 @@
*/
import { Subject } from 'rxjs';
+import { Filter, Query } from 'src/plugins/data/public';
+
+export interface MapsAppState {
+ query?: Query | null;
+ savedQueryId?: string;
+ filters?: Filter[];
+}
export class AppStateManager {
- _query = '';
- _savedQuery = '';
- _filters = [];
+ _query: Query | null = null;
+ _savedQueryId: string = '';
+ _filters: Filter[] = [];
_updated$ = new Subject();
- setQueryAndFilters({ query, savedQuery, filters }) {
+ setQueryAndFilters({ query, savedQueryId, filters }: MapsAppState) {
if (query && this._query !== query) {
this._query = query;
}
- if (savedQuery && this._savedQuery !== savedQuery) {
- this._savedQuery = savedQuery;
+ if (savedQueryId && this._savedQueryId !== savedQueryId) {
+ this._savedQueryId = savedQueryId;
}
if (filters && this._filters !== filters) {
this._filters = filters;
@@ -34,10 +41,10 @@ export class AppStateManager {
return this._filters;
}
- getAppState() {
+ getAppState(): MapsAppState {
return {
query: this._query,
- savedQuery: this._savedQuery,
+ savedQueryId: this._savedQueryId,
filters: this._filters,
};
}
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
similarity index 88%
rename from x-pack/plugins/maps/public/routing/state_syncing/app_sync.js
rename to x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
index 60e8dc9cd574c..b346822913bec 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js
+++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public';
-import { syncState } from '../../../../../../src/plugins/kibana_utils/public';
import { map } from 'rxjs/operators';
+import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public';
+import { syncState, BaseStateContainer } from '../../../../../../src/plugins/kibana_utils/public';
import { getData } from '../../kibana_services';
import { kbnUrlStateStorage } from '../maps_router';
+import { AppStateManager } from './app_state_manager';
-export function startAppStateSyncing(appStateManager) {
+export function startAppStateSyncing(appStateManager: AppStateManager) {
// get appStateContainer
// sync app filters with app state container from data.query to state container
const { query } = getData();
@@ -19,7 +20,7 @@ export function startAppStateSyncing(appStateManager) {
// clear app state filters to prevent application filters from other applications being transfered to maps
query.filterManager.setAppFilters([]);
- const stateContainer = {
+ const stateContainer: BaseStateContainer = {
get: () => ({
query: appStateManager.getQuery(),
filters: appStateManager.getFilters(),
@@ -48,6 +49,7 @@ export function startAppStateSyncing(appStateManager) {
// merge initial state from app state container and current state in url
const initialAppState = {
...stateContainer.get(),
+ // @ts-ignore
...kbnUrlStateStorage.get('_a'),
};
// trigger state update. actually needed in case some data was in url
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
index 4e17241752f53..1e779831c5e0c 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
+++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
@@ -3,27 +3,30 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { TimeRange, RefreshInterval, Filter } from 'src/plugins/data/public';
import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public';
import { getData } from '../../kibana_services';
-// @ts-ignore
import { kbnUrlStateStorage } from '../maps_router';
+export interface MapsGlobalState {
+ time?: TimeRange;
+ refreshInterval?: RefreshInterval;
+ filters?: Filter[];
+}
+
export function startGlobalStateSyncing() {
const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage);
return stop;
}
-export function getGlobalState() {
- return kbnUrlStateStorage.get('_g');
+export function getGlobalState(): MapsGlobalState {
+ return kbnUrlStateStorage.get('_g') as MapsGlobalState;
}
-export function updateGlobalState(newState: unknown, flushUrlState = false) {
+export function updateGlobalState(newState: MapsGlobalState, flushUrlState = false) {
const globalState = getGlobalState();
kbnUrlStateStorage.set('_g', {
- // @ts-ignore
...globalState,
- // @ts-ignore
...newState,
});
if (flushUrlState) {
diff --git a/x-pack/plugins/maps/public/routing/store_operations.js b/x-pack/plugins/maps/public/routing/store_operations.ts
similarity index 100%
rename from x-pack/plugins/maps/public/routing/store_operations.js
rename to x-pack/plugins/maps/public/routing/store_operations.ts
diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
new file mode 100644
index 0000000000000..830537cbadbc8
--- /dev/null
+++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const DEFAULT_RESULTS_FIELD = 'ml';
diff --git a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
index 18a49bb3841b3..6bc0e55b5aadd 100644
--- a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
+++ b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
@@ -69,9 +69,7 @@
"datafeed_id": "datafeed-farequote",
"job_id": "farequote",
"query_delay": "115823ms",
- "indices": [
- "farequote"
- ],
+ "indices": ["farequote"],
"query": {
"bool": {
"must": [
@@ -103,7 +101,7 @@
"buckets": {
"date_histogram": {
"field": "@timestamp",
- "interval": 900000,
+ "fixed_interval": "15m",
"offset": 0,
"order": {
"_key": "asc"
diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
index f0aac75047585..60d2ca63dda59 100644
--- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
@@ -79,3 +79,9 @@ export interface DataFrameAnalyticsConfig {
version: string;
allow_lazy_start?: boolean;
}
+
+export enum ANALYSIS_CONFIG_TYPE {
+ OUTLIER_DETECTION = 'outlier_detection',
+ REGRESSION = 'regression',
+ CLASSIFICATION = 'classification',
+}
diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts
new file mode 100644
index 0000000000000..d2ab9f6c58608
--- /dev/null
+++ b/x-pack/plugins/ml/common/types/feature_importance.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ClassFeatureImportance {
+ class_name: string | boolean;
+ importance: number;
+}
+export interface FeatureImportance {
+ feature_name: string;
+ importance?: number;
+ classes?: ClassFeatureImportance[];
+}
+
+export interface TopClass {
+ class_name: string;
+ class_probability: number;
+ class_score: number;
+}
+
+export type TopClasses = TopClass[];
diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
index c997a4e24f868..a8b775c8d5f60 100644
--- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts
+++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
@@ -84,7 +84,12 @@ export interface Settings {
}
export interface Mappings {
- [key: string]: any;
+ _meta?: {
+ created_by: string;
+ };
+ properties: {
+ [key: string]: any;
+ };
}
export interface IngestPipelineWrapper {
diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts
new file mode 100644
index 0000000000000..d725984a47d66
--- /dev/null
+++ b/x-pack/plugins/ml/common/util/analytics_utils.ts
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnalysisConfig,
+ ClassificationAnalysis,
+ OutlierAnalysis,
+ RegressionAnalysis,
+ ANALYSIS_CONFIG_TYPE,
+} from '../types/data_frame_analytics';
+
+export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
+};
+
+export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
+};
+
+export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
+};
+
+export const getDependentVar = (
+ analysis: AnalysisConfig
+):
+ | RegressionAnalysis['regression']['dependent_variable']
+ | ClassificationAnalysis['classification']['dependent_variable'] => {
+ let depVar = '';
+
+ if (isRegressionAnalysis(analysis)) {
+ depVar = analysis.regression.dependent_variable;
+ }
+
+ if (isClassificationAnalysis(analysis)) {
+ depVar = analysis.classification.dependent_variable;
+ }
+ return depVar;
+};
+
+export const getPredictionFieldName = (
+ analysis: AnalysisConfig
+):
+ | RegressionAnalysis['regression']['prediction_field_name']
+ | ClassificationAnalysis['classification']['prediction_field_name'] => {
+ // If undefined will be defaulted to dependent_variable when config is created
+ let predictionFieldName;
+ if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) {
+ predictionFieldName = analysis.regression.prediction_field_name;
+ } else if (
+ isClassificationAnalysis(analysis) &&
+ analysis.classification.prediction_field_name !== undefined
+ ) {
+ predictionFieldName = analysis.classification.prediction_field_name;
+ }
+ return predictionFieldName;
+};
+
+export const getDefaultPredictionFieldName = (analysis: AnalysisConfig) => {
+ return `${getDependentVar(analysis)}_prediction`;
+};
+export const getPredictedFieldName = (
+ resultsField: string,
+ analysis: AnalysisConfig,
+ forSort?: boolean
+) => {
+ // default is 'ml'
+ const predictionFieldName = getPredictionFieldName(analysis);
+ const predictedField = `${resultsField}.${
+ predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(analysis)
+ }`;
+ return predictedField;
+};
diff --git a/x-pack/plugins/ml/common/util/errors.ts b/x-pack/plugins/ml/common/util/errors.ts
index 6c5fa7bd75daf..a5f89db96cfd7 100644
--- a/x-pack/plugins/ml/common/util/errors.ts
+++ b/x-pack/plugins/ml/common/util/errors.ts
@@ -135,7 +135,14 @@ export const extractErrorProperties = (
typeof error.body.attributes === 'object' &&
error.body.attributes.body?.status !== undefined
) {
- statusCode = error.body.attributes.body?.status;
+ statusCode = error.body.attributes.body.status;
+
+ if (typeof error.body.attributes.body.error?.reason === 'string') {
+ return {
+ message: error.body.attributes.body.error.reason,
+ statusCode,
+ };
+ }
}
if (typeof error.body.message === 'string') {
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
index 1f0fcb63f019d..f252729cc20cd 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
@@ -119,13 +119,14 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
schema = 'numeric';
}
- if (
- field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`) ||
- field.includes(`${resultsField}.${TOP_CLASSES}`)
- ) {
+ if (field.includes(`${resultsField}.${TOP_CLASSES}`)) {
schema = 'json';
}
+ if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) {
+ schema = 'featureImportance';
+ }
+
return { id: field, schema, isSortable };
});
};
@@ -250,10 +251,6 @@ export const useRenderCellValue = (
return cellValue ? 'true' : 'false';
}
- if (typeof cellValue === 'object' && cellValue !== null) {
- return JSON.stringify(cellValue);
- }
-
return cellValue;
};
}, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]);
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
index d4be2eab13d26..22815fe593d57 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
@@ -5,8 +5,7 @@
*/
import { isEqual } from 'lodash';
-import React, { memo, useEffect, FC } from 'react';
-
+import React, { memo, useEffect, FC, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
@@ -24,13 +23,16 @@ import {
} from '@elastic/eui';
import { CoreSetup } from 'src/core/public';
-
import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_histograms';
-import { INDEX_STATUS } from '../../data_frame_analytics/common';
+import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common';
import { euiDataGridStyle, euiDataGridToolbarSettings } from './common';
import { UseIndexDataReturnType } from './types';
+import { DecisionPathPopover } from './feature_importance/decision_path_popover';
+import { TopClasses } from '../../../../common/types/feature_importance';
+import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics';
+
// TODO Fix row hovering + bar highlighting
// import { hoveredRow$ } from './column_chart';
@@ -41,6 +43,9 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
);
interface PropsWithoutHeader extends UseIndexDataReturnType {
+ baseline?: number;
+ analysisType?: ANALYSIS_CONFIG_TYPE;
+ resultsField?: string;
dataTestSubj: string;
toastNotifications: CoreSetup['notifications']['toasts'];
}
@@ -60,6 +65,7 @@ type Props = PropsWithHeader | PropsWithoutHeader;
export const DataGrid: FC = memo(
(props) => {
const {
+ baseline,
chartsVisible,
chartsButtonVisible,
columnsWithCharts,
@@ -80,8 +86,10 @@ export const DataGrid: FC = memo(
toastNotifications,
toggleChartVisibility,
visibleColumns,
+ predictionFieldName,
+ resultsField,
+ analysisType,
} = props;
-
// TODO Fix row hovering + bar highlighting
// const getRowProps = (item: any) => {
// return {
@@ -90,6 +98,45 @@ export const DataGrid: FC = memo(
// };
// };
+ const popOverContent = useMemo(() => {
+ return analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
+ analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
+ ? {
+ featureImportance: ({ children }: { cellContentsElement: any; children: any }) => {
+ const rowIndex = children?.props?.visibleRowIndex;
+ const row = data[rowIndex];
+ if (!row) return ;
+ // if resultsField for some reason is not available then use ml
+ const mlResultsField = resultsField ?? DEFAULT_RESULTS_FIELD;
+ const parsedFIArray = row[mlResultsField].feature_importance;
+ let predictedValue: string | number | undefined;
+ let topClasses: TopClasses = [];
+ if (
+ predictionFieldName !== undefined &&
+ row &&
+ row[mlResultsField][predictionFieldName] !== undefined
+ ) {
+ predictedValue = row[mlResultsField][predictionFieldName];
+ topClasses = row[mlResultsField].top_classes;
+ }
+
+ return (
+
+ );
+ },
+ }
+ : undefined;
+ }, [baseline, data]);
+
useEffect(() => {
if (invalidSortingColumnns.length > 0) {
invalidSortingColumnns.forEach((columnId) => {
@@ -225,6 +272,7 @@ export const DataGrid: FC = memo(
}
: {}),
}}
+ popoverContents={popOverContent}
pagination={{
...pagination,
pageSizeOptions: [5, 10, 25],
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx
new file mode 100644
index 0000000000000..b546ac1db57dd
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnnotationDomainTypes,
+ Axis,
+ AxisStyle,
+ Chart,
+ LineAnnotation,
+ LineAnnotationStyle,
+ LineAnnotationDatum,
+ LineSeries,
+ PartialTheme,
+ Position,
+ RecursivePartial,
+ ScaleType,
+ Settings,
+} from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+
+import React, { useCallback, useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import euiVars from '@elastic/eui/dist/eui_theme_light.json';
+import { DecisionPathPlotData } from './use_classification_path_data';
+
+const { euiColorFullShade, euiColorMediumShade } = euiVars;
+const axisColor = euiColorMediumShade;
+
+const baselineStyle: LineAnnotationStyle = {
+ line: {
+ strokeWidth: 1,
+ stroke: euiColorFullShade,
+ opacity: 0.75,
+ },
+ details: {
+ fontFamily: 'Arial',
+ fontSize: 10,
+ fontStyle: 'bold',
+ fill: euiColorMediumShade,
+ padding: 0,
+ },
+};
+
+const axes: RecursivePartial = {
+ axisLine: {
+ stroke: axisColor,
+ },
+ tickLabel: {
+ fontSize: 10,
+ fill: axisColor,
+ },
+ tickLine: {
+ stroke: axisColor,
+ },
+ gridLine: {
+ horizontal: {
+ dash: [1, 2],
+ },
+ vertical: {
+ strokeWidth: 0,
+ },
+ },
+};
+const theme: PartialTheme = {
+ axes,
+};
+
+interface DecisionPathChartProps {
+ decisionPathData: DecisionPathPlotData;
+ predictionFieldName?: string;
+ baseline?: number;
+ minDomain: number | undefined;
+ maxDomain: number | undefined;
+}
+
+const DECISION_PATH_MARGIN = 125;
+const DECISION_PATH_ROW_HEIGHT = 10;
+const NUM_PRECISION = 3;
+const AnnotationBaselineMarker = ;
+
+export const DecisionPathChart = ({
+ decisionPathData,
+ predictionFieldName,
+ minDomain,
+ maxDomain,
+ baseline,
+}: DecisionPathChartProps) => {
+ // adjust the height so it's compact for items with more features
+ const baselineData: LineAnnotationDatum[] = useMemo(
+ () => [
+ {
+ dataValue: baseline,
+ header: baseline ? baseline.toPrecision(NUM_PRECISION) : '',
+ details: i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText',
+ {
+ defaultMessage:
+ 'baseline (average of predictions for all data points in the training data set)',
+ }
+ ),
+ },
+ ],
+ [baseline]
+ );
+ // guarantee up to num_precision significant digits
+ // without having it in scientific notation
+ const tickFormatter = useCallback((d) => Number(d.toPrecision(NUM_PRECISION)).toString(), []);
+
+ return (
+
+
+ {baseline && (
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx
new file mode 100644
index 0000000000000..bd001fa81a582
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useMemo, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiTitle } from '@elastic/eui';
+import d3 from 'd3';
+import {
+ isDecisionPathData,
+ useDecisionPathData,
+ getStringBasedClassName,
+} from './use_classification_path_data';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { DecisionPathChart } from './decision_path_chart';
+import { MissingDecisionPathCallout } from './missing_decision_path_callout';
+
+interface ClassificationDecisionPathProps {
+ predictedValue: string | boolean;
+ predictionFieldName?: string;
+ featureImportance: FeatureImportance[];
+ topClasses: TopClasses;
+}
+
+export const ClassificationDecisionPath: FC = ({
+ featureImportance,
+ predictedValue,
+ topClasses,
+ predictionFieldName,
+}) => {
+ const [currentClass, setCurrentClass] = useState(
+ getStringBasedClassName(topClasses[0].class_name)
+ );
+ const { decisionPathData } = useDecisionPathData({
+ featureImportance,
+ predictedValue: currentClass,
+ });
+ const options = useMemo(() => {
+ const predictionValueStr = getStringBasedClassName(predictedValue);
+
+ return Array.isArray(topClasses)
+ ? topClasses.map((c) => {
+ const className = getStringBasedClassName(c.class_name);
+ return {
+ value: className,
+ inputDisplay:
+ className === predictionValueStr ? (
+
+ {className}
+
+ ) : (
+ className
+ ),
+ };
+ })
+ : undefined;
+ }, [topClasses, predictedValue]);
+
+ const domain = useMemo(() => {
+ let maxDomain;
+ let minDomain;
+ // if decisionPathData has calculated cumulative path
+ if (decisionPathData && isDecisionPathData(decisionPathData)) {
+ const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]);
+ const buffer = Math.abs(max - min) * 0.1;
+ maxDomain = max + buffer;
+ minDomain = min - buffer;
+ }
+ return { maxDomain, minDomain };
+ }, [decisionPathData]);
+
+ if (!decisionPathData) return ;
+
+ return (
+ <>
+
+
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle',
+ {
+ defaultMessage: 'Class name',
+ }
+ )}
+
+
+ {options !== undefined && (
+
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx
new file mode 100644
index 0000000000000..343324b27f9b5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { EuiCodeBlock } from '@elastic/eui';
+import { FeatureImportance } from '../../../../../common/types/feature_importance';
+
+interface DecisionPathJSONViewerProps {
+ featureImportance: FeatureImportance[];
+}
+export const DecisionPathJSONViewer: FC = ({ featureImportance }) => {
+ return {JSON.stringify(featureImportance)};
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
new file mode 100644
index 0000000000000..263337f93e9a8
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState } from 'react';
+import { EuiLink, EuiTab, EuiTabs, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { RegressionDecisionPath } from './decision_path_regression';
+import { DecisionPathJSONViewer } from './decision_path_json_viewer';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common';
+import { ClassificationDecisionPath } from './decision_path_classification';
+import { useMlKibana } from '../../../contexts/kibana';
+
+interface DecisionPathPopoverProps {
+ featureImportance: FeatureImportance[];
+ analysisType: ANALYSIS_CONFIG_TYPE;
+ predictionFieldName?: string;
+ baseline?: number;
+ predictedValue?: number | string | undefined;
+ topClasses?: TopClasses;
+}
+
+enum DECISION_PATH_TABS {
+ CHART = 'decision_path_chart',
+ JSON = 'decision_path_json',
+}
+
+export interface ExtendedFeatureImportance extends FeatureImportance {
+ absImportance?: number;
+}
+
+export const DecisionPathPopover: FC = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+ topClasses,
+ analysisType,
+ predictionFieldName,
+}) => {
+ const [selectedTabId, setSelectedTabId] = useState(DECISION_PATH_TABS.CHART);
+ const {
+ services: { docLinks },
+ } = useMlKibana();
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
+
+ if (featureImportance.length < 2) {
+ return ;
+ }
+
+ const tabs = [
+ {
+ id: DECISION_PATH_TABS.CHART,
+ name: (
+
+ ),
+ },
+ {
+ id: DECISION_PATH_TABS.JSON,
+ name: (
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+
+ {tabs.map((tab) => (
+ setSelectedTabId(tab.id)}
+ key={tab.id}
+ >
+ {tab.name}
+
+ ))}
+
+
+ {selectedTabId === DECISION_PATH_TABS.CHART && (
+ <>
+
+
+
+
+ ),
+ }}
+ />
+
+ {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
+
+ )}
+ {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
+
+ )}
+ >
+ )}
+ {selectedTabId === DECISION_PATH_TABS.JSON && (
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx
new file mode 100644
index 0000000000000..345269a944f02
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useMemo } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import d3 from 'd3';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { useDecisionPathData, isDecisionPathData } from './use_classification_path_data';
+import { DecisionPathChart } from './decision_path_chart';
+import { MissingDecisionPathCallout } from './missing_decision_path_callout';
+
+interface RegressionDecisionPathProps {
+ predictionFieldName?: string;
+ baseline?: number;
+ predictedValue?: number | undefined;
+ featureImportance: FeatureImportance[];
+ topClasses?: TopClasses;
+}
+
+export const RegressionDecisionPath: FC = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+ predictionFieldName,
+}) => {
+ const { decisionPathData } = useDecisionPathData({
+ baseline,
+ featureImportance,
+ predictedValue,
+ });
+ const domain = useMemo(() => {
+ let maxDomain;
+ let minDomain;
+ // if decisionPathData has calculated cumulative path
+ if (decisionPathData && isDecisionPathData(decisionPathData)) {
+ const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]);
+ maxDomain = max;
+ minDomain = min;
+ const buffer = Math.abs(maxDomain - minDomain) * 0.1;
+ maxDomain =
+ (typeof baseline === 'number' ? Math.max(maxDomain, baseline) : maxDomain) + buffer;
+ minDomain =
+ (typeof baseline === 'number' ? Math.min(minDomain, baseline) : minDomain) - buffer;
+ }
+ return { maxDomain, minDomain };
+ }, [decisionPathData, baseline]);
+
+ if (!decisionPathData) return ;
+
+ return (
+ <>
+ {baseline === undefined && (
+
+ }
+ color="warning"
+ iconType="alert"
+ />
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx
new file mode 100644
index 0000000000000..66eb2047b1314
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const MissingDecisionPathCallout = () => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx
new file mode 100644
index 0000000000000..90216c4a58ffc
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx
@@ -0,0 +1,173 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { ExtendedFeatureImportance } from './decision_path_popover';
+
+export type DecisionPathPlotData = Array<[string, number, number]>;
+
+interface UseDecisionPathDataParams {
+ featureImportance: FeatureImportance[];
+ baseline?: number;
+ predictedValue?: string | number | undefined;
+ topClasses?: TopClasses;
+}
+
+interface RegressionDecisionPathProps {
+ baseline?: number;
+ predictedValue?: number | undefined;
+ featureImportance: FeatureImportance[];
+ topClasses?: TopClasses;
+}
+const FEATURE_NAME = 'feature_name';
+const FEATURE_IMPORTANCE = 'importance';
+
+export const isDecisionPathData = (decisionPathData: any): boolean => {
+ return (
+ Array.isArray(decisionPathData) &&
+ decisionPathData.length > 0 &&
+ decisionPathData[0].length === 3
+ );
+};
+
+// cast to 'True' | 'False' | value to match Eui display
+export const getStringBasedClassName = (v: string | boolean | undefined | number): string => {
+ if (v === undefined) {
+ return '';
+ }
+ if (typeof v === 'boolean') {
+ return v ? 'True' : 'False';
+ }
+ if (typeof v === 'number') {
+ return v.toString();
+ }
+ return v;
+};
+
+export const useDecisionPathData = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+}: UseDecisionPathDataParams): { decisionPathData: DecisionPathPlotData | undefined } => {
+ const decisionPathData = useMemo(() => {
+ return baseline
+ ? buildRegressionDecisionPathData({
+ baseline,
+ featureImportance,
+ predictedValue: predictedValue as number | undefined,
+ })
+ : buildClassificationDecisionPathData({
+ featureImportance,
+ currentClass: predictedValue as string | undefined,
+ });
+ }, [baseline, featureImportance, predictedValue]);
+
+ return { decisionPathData };
+};
+
+export const buildDecisionPathData = (featureImportance: ExtendedFeatureImportance[]) => {
+ const finalResult: DecisionPathPlotData = featureImportance
+ // sort so absolute importance so it goes from bottom (baseline) to top
+ .sort(
+ (a: ExtendedFeatureImportance, b: ExtendedFeatureImportance) =>
+ b.absImportance! - a.absImportance!
+ )
+ .map((d) => [d[FEATURE_NAME] as string, d[FEATURE_IMPORTANCE] as number, NaN]);
+
+ // start at the baseline and end at predicted value
+ // for regression, cumulativeSum should add up to baseline
+ let cumulativeSum = 0;
+ for (let i = featureImportance.length - 1; i >= 0; i--) {
+ cumulativeSum += finalResult[i][1];
+ finalResult[i][2] = cumulativeSum;
+ }
+ return finalResult;
+};
+export const buildRegressionDecisionPathData = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+}: RegressionDecisionPathProps): DecisionPathPlotData | undefined => {
+ let mappedFeatureImportance: ExtendedFeatureImportance[] = featureImportance;
+ mappedFeatureImportance = mappedFeatureImportance.map((d) => ({
+ ...d,
+ absImportance: Math.abs(d[FEATURE_IMPORTANCE] as number),
+ }));
+
+ if (baseline && predictedValue !== undefined && Number.isFinite(predictedValue)) {
+ // get the adjusted importance needed for when # of fields included in c++ analysis != max allowed
+ // if num fields included = num features allowed exactly, adjustedImportance should be 0
+ const adjustedImportance =
+ predictedValue -
+ mappedFeatureImportance.reduce(
+ (accumulator, currentValue) => accumulator + currentValue.importance!,
+ 0
+ ) -
+ baseline;
+
+ mappedFeatureImportance.push({
+ [FEATURE_NAME]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle',
+ {
+ defaultMessage: 'baseline',
+ }
+ ),
+ [FEATURE_IMPORTANCE]: baseline,
+ absImportance: -1,
+ });
+
+ // if the difference is small enough then no need to plot the residual feature importance
+ if (Math.abs(adjustedImportance) > 1e-5) {
+ mappedFeatureImportance.push({
+ [FEATURE_NAME]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle',
+ {
+ defaultMessage: 'other',
+ }
+ ),
+ [FEATURE_IMPORTANCE]: adjustedImportance,
+ absImportance: 0, // arbitrary importance so this will be of higher importance than baseline
+ });
+ }
+ }
+ const filteredFeatureImportance = mappedFeatureImportance.filter(
+ (f) => f !== undefined
+ ) as ExtendedFeatureImportance[];
+
+ return buildDecisionPathData(filteredFeatureImportance);
+};
+
+export const buildClassificationDecisionPathData = ({
+ featureImportance,
+ currentClass,
+}: {
+ featureImportance: FeatureImportance[];
+ currentClass: string | undefined;
+}): DecisionPathPlotData | undefined => {
+ if (currentClass === undefined) return [];
+ const mappedFeatureImportance: Array<
+ ExtendedFeatureImportance | undefined
+ > = featureImportance.map((feature) => {
+ const classFeatureImportance = Array.isArray(feature.classes)
+ ? feature.classes.find((c) => getStringBasedClassName(c.class_name) === currentClass)
+ : feature;
+ if (classFeatureImportance && typeof classFeatureImportance[FEATURE_IMPORTANCE] === 'number') {
+ return {
+ [FEATURE_NAME]: feature[FEATURE_NAME],
+ [FEATURE_IMPORTANCE]: classFeatureImportance[FEATURE_IMPORTANCE],
+ absImportance: Math.abs(classFeatureImportance[FEATURE_IMPORTANCE] as number),
+ };
+ }
+ return undefined;
+ });
+ const filteredFeatureImportance = mappedFeatureImportance.filter(
+ (f) => f !== undefined
+ ) as ExtendedFeatureImportance[];
+
+ return buildDecisionPathData(filteredFeatureImportance);
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
index 756f74c8f9302..f9ee8c37fabf7 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
@@ -74,6 +74,9 @@ export interface UseIndexDataReturnType
| 'tableItems'
| 'toggleChartVisibility'
| 'visibleColumns'
+ | 'baseline'
+ | 'predictionFieldName'
+ | 'resultsField'
> {
renderCellValue: RenderCellValue;
}
@@ -105,4 +108,7 @@ export interface UseDataGridReturnType {
tableItems: DataGridItem[];
toggleChartVisibility: () => void;
visibleColumns: ColumnId[];
+ baseline?: number;
+ predictionFieldName?: string;
+ resultsField?: string;
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index 8ad861e616b7a..97098ea9e75c6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -15,18 +15,19 @@ import { SavedSearchQuery } from '../../contexts/ml';
import {
AnalysisConfig,
ClassificationAnalysis,
- OutlierAnalysis,
RegressionAnalysis,
+ ANALYSIS_CONFIG_TYPE,
} from '../../../../common/types/data_frame_analytics';
-
+import {
+ isOutlierAnalysis,
+ isRegressionAnalysis,
+ isClassificationAnalysis,
+ getPredictionFieldName,
+ getDependentVar,
+ getPredictedFieldName,
+} from '../../../../common/util/analytics_utils';
export type IndexPattern = string;
-export enum ANALYSIS_CONFIG_TYPE {
- OUTLIER_DETECTION = 'outlier_detection',
- REGRESSION = 'regression',
- CLASSIFICATION = 'classification',
-}
-
export enum ANALYSIS_ADVANCED_FIELDS {
ETA = 'eta',
FEATURE_BAG_FRACTION = 'feature_bag_fraction',
@@ -156,23 +157,6 @@ export const getAnalysisType = (analysis: AnalysisConfig): string => {
return 'unknown';
};
-export const getDependentVar = (
- analysis: AnalysisConfig
-):
- | RegressionAnalysis['regression']['dependent_variable']
- | ClassificationAnalysis['classification']['dependent_variable'] => {
- let depVar = '';
-
- if (isRegressionAnalysis(analysis)) {
- depVar = analysis.regression.dependent_variable;
- }
-
- if (isClassificationAnalysis(analysis)) {
- depVar = analysis.classification.dependent_variable;
- }
- return depVar;
-};
-
export const getTrainingPercent = (
analysis: AnalysisConfig
):
@@ -190,24 +174,6 @@ export const getTrainingPercent = (
return trainingPercent;
};
-export const getPredictionFieldName = (
- analysis: AnalysisConfig
-):
- | RegressionAnalysis['regression']['prediction_field_name']
- | ClassificationAnalysis['classification']['prediction_field_name'] => {
- // If undefined will be defaulted to dependent_variable when config is created
- let predictionFieldName;
- if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) {
- predictionFieldName = analysis.regression.prediction_field_name;
- } else if (
- isClassificationAnalysis(analysis) &&
- analysis.classification.prediction_field_name !== undefined
- ) {
- predictionFieldName = analysis.classification.prediction_field_name;
- }
- return predictionFieldName;
-};
-
export const getNumTopClasses = (
analysis: AnalysisConfig
): ClassificationAnalysis['classification']['num_top_classes'] => {
@@ -238,35 +204,6 @@ export const getNumTopFeatureImportanceValues = (
return numTopFeatureImportanceValues;
};
-export const getPredictedFieldName = (
- resultsField: string,
- analysis: AnalysisConfig,
- forSort?: boolean
-) => {
- // default is 'ml'
- const predictionFieldName = getPredictionFieldName(analysis);
- const defaultPredictionField = `${getDependentVar(analysis)}_prediction`;
- const predictedField = `${resultsField}.${
- predictionFieldName ? predictionFieldName : defaultPredictionField
- }`;
- return predictedField;
-};
-
-export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
-};
-
-export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
-};
-
-export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
-};
-
export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => {
if (arg === undefined) return false;
const keys = Object.keys(arg);
@@ -607,3 +544,13 @@ export const loadDocsCount = async ({
};
}
};
+
+export {
+ isOutlierAnalysis,
+ isRegressionAnalysis,
+ isClassificationAnalysis,
+ getPredictionFieldName,
+ ANALYSIS_CONFIG_TYPE,
+ getDependentVar,
+ getPredictedFieldName,
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
index 2f14dfdfdfca3..c2295a92af89c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
@@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-export const DEFAULT_RESULTS_FIELD = 'ml';
export const FEATURE_IMPORTANCE = 'feature_importance';
export const FEATURE_INFLUENCE = 'feature_influence';
export const TOP_CLASSES = 'top_classes';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
index 847aefefbc6c8..f9c9bf26a9d16 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
@@ -4,17 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getNumTopClasses, getNumTopFeatureImportanceValues } from './analytics';
+import { Field } from '../../../../common/types/fields';
import {
- getNumTopClasses,
- getNumTopFeatureImportanceValues,
getPredictedFieldName,
getDependentVar,
getPredictionFieldName,
isClassificationAnalysis,
isOutlierAnalysis,
isRegressionAnalysis,
-} from './analytics';
-import { Field } from '../../../../common/types/fields';
+} from '../../../../common/util/analytics_utils';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../services/new_job_capabilities_service';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index 25baff98556a6..dd9ecc963840a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -209,7 +209,6 @@ export const ConfigurationStepForm: FC = ({
let unsupportedFieldsErrorMessage;
if (
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
- errorMessage.includes('status_exception') &&
(errorMessage.includes('must have at most') || errorMessage.includes('must have at least'))
) {
maxDistinctValuesErrorMessage = errorMessage;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
index ccac9a697210b..2e3a5d89367ce 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
@@ -9,7 +9,6 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { ExplorationPageWrapper } from '../exploration_page_wrapper';
-
import { EvaluatePanel } from './evaluate_panel';
interface Props {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
index 34ff36c59fa6c..84b44ef0d349f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
@@ -51,7 +51,6 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel
/>
);
}
-
return (
<>
{isLoadingJobConfig === true && jobConfig === undefined && }
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
index 8395a11bd6fda..eea579ef1d064 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
@@ -28,6 +28,8 @@ import {
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
+ getAnalysisType,
+ ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@@ -36,6 +38,7 @@ import { ExplorationQueryBar } from '../exploration_query_bar';
import { IndexPatternPrompt } from '../index_pattern_prompt';
import { useExplorationResults } from './use_exploration_results';
+import { useMlKibana } from '../../../../../contexts/kibana';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText',
@@ -70,18 +73,27 @@ export const ExplorationResultsTable: FC = React.memo(
setEvaluateSearchQuery,
title,
}) => {
+ const {
+ services: {
+ mlServices: { mlApiServices },
+ },
+ } = useMlKibana();
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
useEffect(() => {
setEvaluateSearchQuery(searchQuery);
}, [JSON.stringify(searchQuery)]);
+ const analysisType = getAnalysisType(jobConfig.analysis);
+
const classificationData = useExplorationResults(
indexPattern,
jobConfig,
searchQuery,
- getToastNotifications()
+ getToastNotifications(),
+ mlApiServices
);
+
const docFieldsCount = classificationData.columnsWithCharts.length;
const {
columnsWithCharts,
@@ -94,7 +106,6 @@ export const ExplorationResultsTable: FC = React.memo(
if (jobConfig === undefined || classificationData === undefined) {
return null;
}
-
// if it's a searchBar syntax error leave the table visible so they can try again
if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) {
return (
@@ -184,6 +195,7 @@ export const ExplorationResultsTable: FC = React.memo(
{...classificationData}
dataTestSubj="mlExplorationDataGrid"
toastNotifications={getToastNotifications()}
+ analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE}
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
index 8d53214d23d47..a56345017258e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
@@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import { CoreSetup } from 'src/core/public';
+import { i18n } from '@kbn/i18n';
+import { MlApiServices } from '../../../../../services/ml_api_service';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader';
@@ -23,21 +25,26 @@ import {
UseIndexDataReturnType,
} from '../../../../../components/data_grid';
import { SavedSearchQuery } from '../../../../../contexts/ml';
-
import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common';
import {
- DEFAULT_RESULTS_FIELD,
- FEATURE_IMPORTANCE,
- TOP_CLASSES,
-} from '../../../../common/constants';
+ getPredictionFieldName,
+ getDefaultPredictionFieldName,
+} from '../../../../../../../common/util/analytics_utils';
+import { FEATURE_IMPORTANCE, TOP_CLASSES } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields';
+import { isRegressionAnalysis } from '../../../../common/analytics';
+import { extractErrorMessage } from '../../../../../../../common/util/errors';
export const useExplorationResults = (
indexPattern: IndexPattern | undefined,
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: SavedSearchQuery,
- toastNotifications: CoreSetup['notifications']['toasts']
+ toastNotifications: CoreSetup['notifications']['toasts'],
+ mlApiServices: MlApiServices
): UseIndexDataReturnType => {
+ const [baseline, setBaseLine] = useState();
+
const needsDestIndexFields =
indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0];
@@ -52,7 +59,6 @@ export const useExplorationResults = (
)
);
}
-
const dataGrid = useDataGrid(
columns,
25,
@@ -107,16 +113,60 @@ export const useExplorationResults = (
jobConfig?.dest.index,
JSON.stringify([searchQuery, dataGrid.visibleColumns]),
]);
+ const predictionFieldName = useMemo(() => {
+ if (jobConfig) {
+ return (
+ getPredictionFieldName(jobConfig.analysis) ??
+ getDefaultPredictionFieldName(jobConfig.analysis)
+ );
+ }
+ return undefined;
+ }, [jobConfig]);
+
+ const getAnalyticsBaseline = useCallback(async () => {
+ try {
+ if (
+ jobConfig !== undefined &&
+ jobConfig.analysis !== undefined &&
+ isRegressionAnalysis(jobConfig.analysis)
+ ) {
+ const result = await mlApiServices.dataFrameAnalytics.getAnalyticsBaseline(jobConfig.id);
+ if (result?.baseline) {
+ setBaseLine(result.baseline);
+ }
+ }
+ } catch (e) {
+ const error = extractErrorMessage(e);
+
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast',
+ {
+ defaultMessage: 'An error occurred getting feature importance baseline',
+ }
+ ),
+ text: error,
+ });
+ }
+ }, [mlApiServices, jobConfig]);
+
+ useEffect(() => {
+ getAnalyticsBaseline();
+ }, [jobConfig]);
+ const resultsField = jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD;
const renderCellValue = useRenderCellValue(
indexPattern,
dataGrid.pagination,
dataGrid.tableItems,
- jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD
+ resultsField
);
return {
...dataGrid,
renderCellValue,
+ baseline,
+ predictionFieldName,
+ resultsField,
};
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
index 24649ae5f1e71..151e5ea4e6feb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
@@ -29,7 +29,8 @@ import { SavedSearchQuery } from '../../../../../contexts/ml';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common';
-import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants';
+import { FEATURE_INFLUENCE } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields';
import { getFeatureCount, getOutlierScoreFieldName } from './common';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
index 60c699ba0d370..ce24892c9de45 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
@@ -12,7 +12,7 @@ import { IIndexPattern } from 'src/plugins/data/common';
import { DeepReadonly } from '../../../../../../../common/types/common';
import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common';
import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics';
-import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana';
import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from '../../hooks/use_create_analytics_form';
import { State } from '../../hooks/use_create_analytics_form/state';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 6d73340cc396a..0c3bff58c25cd 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -99,13 +99,7 @@ export const DataFrameAnalyticsList: FC = ({
const [isInitialized, setIsInitialized] = useState(false);
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [filteredAnalytics, setFilteredAnalytics] = useState<{
- active: boolean;
- items: DataFrameAnalyticsListRow[];
- }>({
- active: false,
- items: [],
- });
+ const [filteredAnalytics, setFilteredAnalytics] = useState([]);
const [searchQueryText, setSearchQueryText] = useState('');
const [analytics, setAnalytics] = useState([]);
const [analyticsStats, setAnalyticsStats] = useState(
@@ -129,12 +123,12 @@ export const DataFrameAnalyticsList: FC = ({
blockRefresh
);
- const setQueryClauses = (queryClauses: any) => {
+ const updateFilteredItems = (queryClauses: any) => {
if (queryClauses.length) {
const filtered = filterAnalytics(analytics, queryClauses);
- setFilteredAnalytics({ active: true, items: filtered });
+ setFilteredAnalytics(filtered);
} else {
- setFilteredAnalytics({ active: false, items: [] });
+ setFilteredAnalytics(analytics);
}
};
@@ -146,9 +140,9 @@ export const DataFrameAnalyticsList: FC = ({
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
clauses = query.ast.clauses;
}
- setQueryClauses(clauses);
+ updateFilteredItems(clauses);
} else {
- setQueryClauses([]);
+ updateFilteredItems([]);
}
};
@@ -192,9 +186,9 @@ export const DataFrameAnalyticsList: FC = ({
isMlEnabledInSpace
);
- const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
- filteredAnalytics.active ? filteredAnalytics.items : analytics
- );
+ const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings<
+ DataFrameAnalyticsListRow
+ >(DataFrameAnalyticsListColumn.id, filteredAnalytics);
// Before the analytics have been loaded for the first time, display the loading indicator only.
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
index 57eb9f6857053..052068c30b84c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
@@ -8,7 +8,6 @@ import { useState } from 'react';
import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui';
import sortBy from 'lodash/sortBy';
import get from 'lodash/get';
-import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common';
const PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@@ -19,37 +18,59 @@ const jobPropertyMap = {
Type: 'job_type',
};
-interface AnalyticsBasicTableSettings {
+// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange
+// Can be removed when https://github.com/elastic/eui/issues/4011 is addressed in EUI
+export interface Criteria {
+ page?: {
+ index: number;
+ size: number;
+ };
+ sort?: {
+ field: keyof T;
+ direction: Direction;
+ };
+}
+export interface CriteriaWithPagination extends Criteria {
+ page: {
+ index: number;
+ size: number;
+ };
+}
+
+interface AnalyticsBasicTableSettings {
pageIndex: number;
pageSize: number;
totalItemCount: number;
hidePerPageOptions: boolean;
- sortField: string;
+ sortField: keyof T;
sortDirection: Direction;
}
-interface UseTableSettingsReturnValue {
- onTableChange: EuiBasicTableProps['onChange'];
- pageOfItems: DataFrameAnalyticsListRow[];
- pagination: EuiBasicTableProps['pagination'];
+interface UseTableSettingsReturnValue {
+ onTableChange: EuiBasicTableProps['onChange'];
+ pageOfItems: T[];
+ pagination: EuiBasicTableProps['pagination'];
sorting: EuiTableSortingType;
}
-export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue {
- const [tableSettings, setTableSettings] = useState({
+export function useTableSettings(
+ sortByField: keyof TypeOfItem,
+ items: TypeOfItem[]
+): UseTableSettingsReturnValue {
+ const [tableSettings, setTableSettings] = useState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
totalItemCount: 0,
hidePerPageOptions: false,
- sortField: DataFrameAnalyticsListColumn.id,
+ sortField: sortByField,
sortDirection: 'asc',
});
const getPageOfItems = (
- list: any[],
+ list: TypeOfItem[],
index: number,
size: number,
- sortField: string,
+ sortField: keyof TypeOfItem,
sortDirection: Direction
) => {
list = sortBy(list, (item) =>
@@ -72,13 +93,10 @@ export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSe
};
};
- const onTableChange = ({
+ const onTableChange: EuiBasicTableProps['onChange'] = ({
page = { index: 0, size: PAGE_SIZE },
- sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
- }: {
- page?: { index: number; size: number };
- sort?: { field: string; direction: Direction };
- }) => {
+ sort = { field: sortByField, direction: 'asc' },
+ }: CriteriaWithPagination) => {
const { index, size } = page;
const { field, direction } = sort;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
index 44a6572a3766c..7a366bb63420c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
@@ -20,6 +20,68 @@ import {
Value,
DataFrameAnalyticsListRow,
} from '../analytics_list/common';
+import { ModelItem } from '../models_management/models_list';
+
+export function filterAnalyticsModels(
+ items: ModelItem[],
+ clauses: Array
+) {
+ if (clauses.length === 0) {
+ return items;
+ }
+
+ // keep count of the number of matches we make as we're looping over the clauses
+ // we only want to return items which match all clauses, i.e. each search term is ANDed
+ const matches: Record = items.reduce((p: Record, c) => {
+ p[c.model_id] = {
+ model: c,
+ count: 0,
+ };
+ return p;
+ }, {});
+
+ clauses.forEach((c) => {
+ // the search term could be negated with a minus, e.g. -bananas
+ const bool = c.match === 'must';
+ let ms = [];
+
+ if (c.type === 'term') {
+ // filter term based clauses, e.g. bananas
+ // match on model_id and type
+ // if the term has been negated, AND the matches
+ if (bool === true) {
+ ms = items.filter(
+ (item) =>
+ stringMatch(item.model_id, c.value) === bool || stringMatch(item.type, c.value) === bool
+ );
+ } else {
+ ms = items.filter(
+ (item) =>
+ stringMatch(item.model_id, c.value) === bool && stringMatch(item.type, c.value) === bool
+ );
+ }
+ } else {
+ // filter other clauses, i.e. the filters for type
+ if (Array.isArray(c.value)) {
+ // type value is an array of string(s) e.g. c.value => ['classification']
+ ms = items.filter((item) => {
+ return item.type !== undefined && (c.value as Value[]).includes(item.type);
+ });
+ } else {
+ ms = items.filter((item) => item[c.field as keyof typeof item] === c.value);
+ }
+ }
+
+ ms.forEach((j) => matches[j.model_id].count++);
+ });
+
+ // loop through the matches and return only those items which have match all the clauses
+ const filtered = Object.values(matches)
+ .filter((m) => (m && m.count) >= clauses.length)
+ .map((m) => m.model);
+
+ return filtered;
+}
export function filterAnalytics(
items: DataFrameAnalyticsListRow[],
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
index 3b901f5063eb1..2748764d7f46e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar';
+export { AnalyticsSearchBar, filterAnalytics, filterAnalyticsModels } from './analytics_search_bar';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
index 3104ec55c3a6d..338b6444671a6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
@@ -4,20 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useState, useCallback, useMemo } from 'react';
+import React, { FC, useState, useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- Direction,
+ EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
- EuiInMemoryTable,
EuiTitle,
EuiButton,
- EuiSearchBarProps,
+ EuiSearchBar,
EuiSpacer,
EuiButtonIcon,
EuiBadge,
+ SearchFilterConfig,
} from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
@@ -42,6 +42,8 @@ import {
refreshAnalyticsList$,
useRefreshAnalyticsList,
} from '../../../../common';
+import { useTableSettings } from '../analytics_list/use_table_settings';
+import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar';
type Stats = Omit;
@@ -66,22 +68,41 @@ export const ModelsList: FC = () => {
const { toasts } = useNotifications();
const [searchQueryText, setSearchQueryText] = useState('');
-
- const [pageIndex, setPageIndex] = useState(0);
- const [pageSize, setPageSize] = useState(10);
- const [sortField, setSortField] = useState(ModelsTableToConfigMapping.id);
- const [sortDirection, setSortDirection] = useState('asc');
-
+ const [filteredModels, setFilteredModels] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [items, setItems] = useState([]);
const [selectedModels, setSelectedModels] = useState([]);
-
const [modelsToDelete, setModelsToDelete] = useState([]);
-
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>(
{}
);
+ const updateFilteredItems = (queryClauses: any) => {
+ if (queryClauses.length) {
+ const filtered = filterAnalyticsModels(items, queryClauses);
+ setFilteredModels(filtered);
+ } else {
+ setFilteredModels(items);
+ }
+ };
+
+ const filterList = () => {
+ if (searchQueryText !== '') {
+ const query = EuiSearchBar.Query.parse(searchQueryText);
+ let clauses: any = [];
+ if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
+ clauses = query.ast.clauses;
+ }
+ updateFilteredItems(clauses);
+ } else {
+ updateFilteredItems([]);
+ }
+ };
+
+ useEffect(() => {
+ filterList();
+ }, [searchQueryText, items]);
+
/**
* Fetches inference trained models.
*/
@@ -355,91 +376,51 @@ export const ModelsList: FC = () => {
},
];
- const pagination = {
- initialPageIndex: pageIndex,
- initialPageSize: pageSize,
- totalItemCount: items.length,
- pageSizeOptions: [10, 20, 50],
- hidePerPageOptions: false,
- };
+ const filters: SearchFilterConfig[] =
+ inferenceTypesOptions && inferenceTypesOptions.length > 0
+ ? [
+ {
+ type: 'field_value_selection',
+ field: 'type',
+ name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
+ defaultMessage: 'Type',
+ }),
+ multiSelect: 'or',
+ options: inferenceTypesOptions,
+ },
+ ]
+ : [];
- const sorting = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
- const search: EuiSearchBarProps = {
- query: searchQueryText,
- onChange: (searchChange) => {
- if (searchChange.error !== null) {
- return false;
- }
- setSearchQueryText(searchChange.queryText);
- return true;
- },
- box: {
- incremental: true,
- },
- ...(inferenceTypesOptions && inferenceTypesOptions.length > 0
- ? {
- filters: [
- {
- type: 'field_value_selection',
- field: 'type',
- name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
- defaultMessage: 'Type',
- }),
- multiSelect: 'or',
- options: inferenceTypesOptions,
- },
- ],
- }
- : {}),
- ...(selectedModels.length > 0
- ? {
- toolsLeft: (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- }
- : {}),
- };
+ const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
+ ModelsTableToConfigMapping.id,
+ filteredModels
+ );
- const onTableChange: EuiInMemoryTable['onTableChange'] = ({
- page = { index: 0, size: 10 },
- sort = { field: ModelsTableToConfigMapping.id, direction: 'asc' },
- }) => {
- const { index, size } = page;
- setPageIndex(index);
- setPageSize(size);
-
- const { field, direction } = sort;
- setSortField(field);
- setSortDirection(direction);
- };
+ const toolsLeft = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
const isSelectionAllowed = canDeleteDataFrameAnalytics;
@@ -473,21 +454,31 @@ export const ModelsList: FC = () => {
-
+ {selectedModels.length > 0 && toolsLeft}
+
+
+
+
+
+
columns={columns}
hasActions={true}
isExpandable={true}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isSelectable={false}
- items={items}
+ items={pageOfItems}
itemId={ModelsTableToConfigMapping.id}
+ itemIdToExpandedRowMap={itemIdToExpandedRowMap}
loading={isLoading}
- onTableChange={onTableChange}
- pagination={pagination}
- sorting={sorting}
- search={search}
+ onChange={onTableChange}
selection={selection}
+ pagination={pagination!}
+ sorting={sorting}
+ data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
rowProps={(item) => ({
'data-test-subj': `mlModelsTableRow row-${item.model_id}`,
})}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
index 34f86ffa18788..b36eccdde2798 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
@@ -80,7 +80,7 @@ export class DataLoader {
earliest: number | undefined,
latest: number | undefined,
fields: FieldRequestConfig[],
- interval?: string
+ interval?: number
): Promise {
const stats = await ml.getVisualizerFieldStats({
indexPatternTitle: this._indexPatternTitle,
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
index 26ed3152058dd..bad1488166e23 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
@@ -348,7 +348,7 @@ export const Page: FC = () => {
earliest,
latest,
existMetricFields,
- aggInterval.expression
+ aggInterval.asMilliseconds()
);
// Add the metric stats to the existing stats in the corresponding config.
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
index 712b64af2db80..a6fda86f27a7c 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
@@ -111,7 +111,7 @@ export const anomalyDataChange = function (
// Query 1 - load the raw metric data.
function getMetricData(config, range) {
- const { jobId, detectorIndex, entityFields, interval } = config;
+ const { jobId, detectorIndex, entityFields, bucketSpanSeconds } = config;
const job = mlJobService.getJob(jobId);
@@ -122,14 +122,14 @@ export const anomalyDataChange = function (
return mlResultsService
.getMetricData(
config.datafeedConfig.indices,
- config.entityFields,
+ entityFields,
datafeedQuery,
config.metricFunction,
config.metricFieldName,
config.timeField,
range.min,
range.max,
- config.interval
+ bucketSpanSeconds * 1000
)
.toPromise();
} else {
@@ -175,7 +175,14 @@ export const anomalyDataChange = function (
};
return mlResultsService
- .getModelPlotOutput(jobId, detectorIndex, criteriaFields, range.min, range.max, interval)
+ .getModelPlotOutput(
+ jobId,
+ detectorIndex,
+ criteriaFields,
+ range.min,
+ range.max,
+ bucketSpanSeconds * 1000
+ )
.toPromise()
.then((resp) => {
// Return data in format required by the explorer charts.
@@ -218,7 +225,7 @@ export const anomalyDataChange = function (
[config.jobId],
range.min,
range.max,
- config.interval,
+ config.bucketSpanSeconds * 1000,
1,
MAX_SCHEDULED_EVENTS
)
@@ -252,7 +259,7 @@ export const anomalyDataChange = function (
config.timeField,
range.min,
range.max,
- config.interval
+ config.bucketSpanSeconds * 1000
);
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
index 2b250b9622286..af31df863ab76 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
@@ -161,7 +161,7 @@ export class ResultsLoader {
[],
this._lastModelTimeStamp,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
agg.mlModelPlotAgg
)
.toPromise();
@@ -211,7 +211,7 @@ export class ResultsLoader {
[this._jobCreator.jobId],
this._jobCreator.start,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
1
);
@@ -233,7 +233,7 @@ export class ResultsLoader {
this._jobCreator.jobId,
this._jobCreator.start,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
this._detectorSplitFieldFilters
);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
index 51c396518c851..02a6f47bed6c9 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
@@ -32,7 +32,7 @@ export function getScoresByRecord(
jobId: string,
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
firstSplitField: SplitFieldWithValue | null
): Promise {
return new Promise((resolve, reject) => {
@@ -104,7 +104,7 @@ export function getScoresByRecord(
byTime: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
extended_bounds: {
min: earliestMs,
diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
index 2bdb758be874c..3ee50a4759006 100644
--- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
+++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
@@ -180,7 +180,7 @@ export class AnomalyTimelineService {
// Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets
// which wouldn't be the case if e.g. '1M' was used.
- const interval = `${swimlaneBucketInterval.asSeconds()}s`;
+ const intervalMs = swimlaneBucketInterval.asMilliseconds();
let response;
if (viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL) {
@@ -190,7 +190,7 @@ export class AnomalyTimelineService {
jobIds,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- interval,
+ intervalMs,
perPage,
fromPage
);
@@ -201,7 +201,7 @@ export class AnomalyTimelineService {
fieldValues,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- interval,
+ intervalMs,
swimlaneLimit,
perPage,
fromPage,
@@ -269,7 +269,7 @@ export class AnomalyTimelineService {
selectedJobIds,
earliestMs,
latestMs,
- this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's',
+ this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asMilliseconds(),
swimlaneLimit
);
return Object.keys(resp.results);
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
index 9eff86c753da9..e30790c57966b 100644
--- a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
@@ -25,7 +25,7 @@ export const mlForecastService: {
entityFields: any[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
aggType: any
) => Observable;
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.js b/x-pack/plugins/ml/public/application/services/forecast_service.js
index 57e50387a03ab..c13e265b4655c 100644
--- a/x-pack/plugins/ml/public/application/services/forecast_service.js
+++ b/x-pack/plugins/ml/public/application/services/forecast_service.js
@@ -153,7 +153,7 @@ function getForecastData(
entityFields,
earliestMs,
latestMs,
- interval,
+ intervalMs,
aggType
) {
// Extract the partition, by, over fields on which to filter.
@@ -257,7 +257,7 @@ function getForecastData(
times: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
index 7de39d91047ef..434200d0383f5 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
@@ -135,4 +135,10 @@ export const dataFrameAnalytics = {
method: 'GET',
});
},
+ getAnalyticsBaseline(analyticsId: string) {
+ return http({
+ path: `${basePath()}/data_frame/analytics/${analyticsId}/baseline`,
+ method: 'POST',
+ });
+ },
};
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index 184039729f9ef..9d7ce4f3df59b 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -485,7 +485,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
earliest?: number;
latest?: number;
samplerShardSize?: number;
- interval?: string;
+ interval?: number;
fields?: FieldRequestConfig[];
maxExamples?: number;
}) {
diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
index 898ca8894cbda..22f878a337f51 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
@@ -70,7 +70,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: string
+ intervalMs: number
): Observable {
// Build the criteria to use in the bool filter part of the request.
// Add criteria for the time range, entity fields,
@@ -136,7 +136,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
byTime: {
date_histogram: {
field: timeFieldName,
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
},
},
@@ -202,7 +202,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
criteriaFields: any[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
aggType?: { min: any; max: any }
): Observable {
const obj: ModelPlotOutput = {
@@ -291,7 +291,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
},
aggs: {
@@ -446,7 +446,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
jobIds: string[] | undefined,
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
maxJobs: number,
maxEvents: number
): Observable {
@@ -518,7 +518,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index b26528b76037b..aae0cb51aa81d 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -13,7 +13,7 @@ export function resultsServiceProvider(
jobIds: string[],
earliestMs: number,
latestMs: number,
- interval: string | number,
+ intervalMs: number,
perPage?: number,
fromPage?: number
): Promise;
@@ -41,7 +41,7 @@ export function resultsServiceProvider(
influencerFieldValues: string[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
maxResults: number,
perPage: number,
fromPage: number,
@@ -57,8 +57,25 @@ export function resultsServiceProvider(
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: string | number
+ intervalMs: number
+ ): Promise;
+ getEventDistributionData(
+ index: string,
+ splitField: string,
+ filterField: string,
+ query: any,
+ metricFunction: string, // ES aggregation name
+ metricFieldName: string,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ intervalMs: number
+ ): Promise;
+ getRecordMaxScoreByTime(
+ jobId: string,
+ criteriaFields: any[],
+ earliestMs: number,
+ latestMs: number,
+ intervalMs: number
): Promise;
- getEventDistributionData(): Promise;
- getRecordMaxScoreByTime(): Promise;
};
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index ef00c9025763e..fd48845494dfd 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -28,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) {
// Pass an empty array or ['*'] to search over all job IDs.
// Returned response contains a results property, with a key for job
// which has results for the specified time range.
- getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) {
+ getScoresByBucket(jobIds, earliestMs, latestMs, intervalMs, perPage = 10, fromPage = 1) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -116,7 +116,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
extended_bounds: {
min: earliestMs,
@@ -492,7 +492,7 @@ export function resultsServiceProvider(mlApiServices) {
influencerFieldValues,
earliestMs,
latestMs,
- interval,
+ intervalMs,
maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
fromPage = 1,
@@ -615,7 +615,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
@@ -1033,7 +1033,7 @@ export function resultsServiceProvider(mlApiServices) {
// Extra query object can be supplied, or pass null if no additional query.
// Returned response contains a results property, which is an object
// of document counts against time (epoch millis).
- getEventRateData(index, query, timeFieldName, earliestMs, latestMs, interval) {
+ getEventRateData(index, query, timeFieldName, earliestMs, latestMs, intervalMs) {
return new Promise((resolve, reject) => {
const obj = { success: true, results: {} };
@@ -1074,7 +1074,7 @@ export function resultsServiceProvider(mlApiServices) {
eventRate: {
date_histogram: {
field: timeFieldName,
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: earliestMs,
@@ -1118,7 +1118,7 @@ export function resultsServiceProvider(mlApiServices) {
timeFieldName,
earliestMs,
latestMs,
- interval
+ intervalMs
) {
return new Promise((resolve, reject) => {
if (splitField === undefined) {
@@ -1187,7 +1187,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: timeFieldName,
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: AGGREGATION_MIN_DOC_COUNT,
},
aggs: {
@@ -1277,7 +1277,7 @@ export function resultsServiceProvider(mlApiServices) {
// criteria, time range, and aggregation interval.
// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
// 'fieldValue' properties.
- getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) {
+ getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, intervalMs) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -1331,7 +1331,7 @@ export function resultsServiceProvider(mlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
index d1e959b33e5dc..5149fecb0ec26 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
@@ -29,7 +29,7 @@ function getMetricData(
entityFields: EntityField[],
earliestMs: number,
latestMs: number,
- interval: string
+ intervalMs: number
): Observable {
if (
isModelPlotChartableForDetector(job, detectorIndex) &&
@@ -76,7 +76,7 @@ function getMetricData(
criteriaFields,
earliestMs,
latestMs,
- interval
+ intervalMs
);
} else {
const obj: ModelPlotOutput = {
@@ -96,7 +96,7 @@ function getMetricData(
chartConfig.timeField,
earliestMs,
latestMs,
- interval
+ intervalMs
)
.pipe(
map((resp) => {
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 0e99d64cf202f..7d173e161a1cb 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -629,7 +629,7 @@ export class TimeSeriesExplorer extends React.Component {
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression
+ stateUpdate.contextAggregationInterval.asMilliseconds()
)
.toPromise()
.then((resp) => {
@@ -652,7 +652,7 @@ export class TimeSeriesExplorer extends React.Component {
this.getCriteriaFields(detectorIndex, entityControls),
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression
+ stateUpdate.contextAggregationInterval.asMilliseconds()
)
.then((resp) => {
const fullRangeRecordScoreData = processRecordScoreResults(resp.results);
@@ -703,7 +703,7 @@ export class TimeSeriesExplorer extends React.Component {
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression,
+ stateUpdate.contextAggregationInterval.asMilliseconds(),
aggType
)
.toPromise()
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
index 23d1e3f7cc904..ce0d7b0abc3e0 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
@@ -61,7 +61,7 @@ export function getFocusData(
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression
+ focusAggregationInterval.asMilliseconds()
),
// Query 2 - load all the records across selected time range for the chart anomaly markers.
mlResultsService.getRecordsForCriteria(
@@ -77,7 +77,7 @@ export function getFocusData(
[selectedJob.job_id],
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression,
+ focusAggregationInterval.asMilliseconds(),
1,
MAX_SCHEDULED_EVENTS
),
@@ -123,7 +123,7 @@ export function getFocusData(
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression,
+ focusAggregationInterval.asMilliseconds(),
aggType
);
})()
diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
index fd0cab7c0625d..981ffe9618d9f 100644
--- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
+++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
@@ -56,7 +56,7 @@ export function polledDataCheckerFactory({ asCurrentUser }) {
date_histogram: {
min_doc_count: 1,
field: this.timeField,
- interval: `${intervalMs}ms`,
+ fixed_interval: `${intervalMs}ms`,
},
},
},
diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
index 750f0cfc0b4a8..5dd0a5ff563d6 100644
--- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
+++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
@@ -166,7 +166,7 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) {
non_empty_buckets: {
date_histogram: {
field: this.timeField,
- interval: `${intervalMs}ms`,
+ fixed_interval: `${intervalMs}ms`,
},
},
},
diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts
new file mode 100644
index 0000000000000..94f54a5654873
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IScopedClusterClient } from 'kibana/server';
+import {
+ getDefaultPredictionFieldName,
+ getPredictionFieldName,
+ isRegressionAnalysis,
+} from '../../../common/util/analytics_utils';
+import { DEFAULT_RESULTS_FIELD } from '../../../common/constants/data_frame_analytics';
+// Obtains data for the data frame analytics feature importance functionalities
+// such as baseline, decision paths, or importance summary.
+export function analyticsFeatureImportanceProvider({
+ asInternalUser,
+ asCurrentUser,
+}: IScopedClusterClient) {
+ async function getRegressionAnalyticsBaseline(analyticsId: string): Promise {
+ const { body } = await asInternalUser.ml.getDataFrameAnalytics({
+ id: analyticsId,
+ });
+ const jobConfig = body.data_frame_analytics[0];
+ if (!isRegressionAnalysis) return undefined;
+ const destinationIndex = jobConfig.dest.index;
+ const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
+ const mlResultsField = jobConfig.dest?.results_field ?? DEFAULT_RESULTS_FIELD;
+ const predictedField = `${mlResultsField}.${
+ predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(jobConfig.analysis)
+ }`;
+ const isTrainingField = `${mlResultsField}.is_training`;
+
+ const params = {
+ index: destinationIndex,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ term: {
+ [isTrainingField]: true,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ featureImportanceBaseline: {
+ avg: {
+ field: predictedField,
+ },
+ },
+ },
+ },
+ };
+ let baseline;
+ const { body: aggregationResult } = await asCurrentUser.search(params);
+ if (aggregationResult) {
+ baseline = aggregationResult.aggregations.featureImportanceBaseline.value;
+ }
+ return baseline;
+ }
+
+ return {
+ getRegressionAnalyticsBaseline,
+ };
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
index a9865183320d5..084aa08455405 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
@@ -10,7 +10,7 @@
"buckets": {
"date_histogram": {
"field": "timestamp",
- "interval": 3600000
+ "fixed_interval": "1h"
},
"aggregations": {
"timestamp": {
diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
index dbfa4b5656e5f..95c4e79150059 100644
--- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
@@ -468,7 +468,7 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: number,
+ intervalMs: number,
maxExamples: number
): Promise {
// Batch up fields by type, getting stats for multiple fields at a time.
@@ -526,7 +526,7 @@ export class DataVisualizer {
timeFieldName,
earliestMs,
latestMs,
- interval
+ intervalMs
);
batchStats.push(stats);
}
@@ -710,7 +710,7 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: number
+ intervalMs: number
): Promise {
const index = indexPatternTitle;
const size = 0;
@@ -718,11 +718,12 @@ export class DataVisualizer {
// Don't use the sampler aggregation as this can lead to some potentially
// confusing date histogram results depending on the date range of data amongst shards.
+
const aggs = {
eventRate: {
date_histogram: {
field: timeFieldName,
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
},
@@ -756,7 +757,7 @@ export class DataVisualizer {
return {
documentCounts: {
- interval,
+ interval: intervalMs,
buckets,
},
};
diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
index 6108454c08aa7..26dba7c2f00c1 100644
--- a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
+++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
@@ -94,7 +94,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) {
_meta: {
created_by: INDEX_META_DATA_CREATED_BY,
},
- properties: mappings,
+ properties: mappings.properties,
},
};
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
index 9eea1ea2a28ae..128b28a223445 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
@@ -114,7 +114,7 @@ function getSearchJsonFromConfig(
times: {
date_histogram: {
field: timeField,
- interval: intervalMs,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: start,
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
index 567afec809405..71e81158d8885 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
@@ -142,7 +142,7 @@ function getPopulationSearchJsonFromConfig(
times: {
date_histogram: {
field: timeField,
- interval: intervalMs,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: start,
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
index ca356b2bede22..9e2af76264231 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
@@ -20,7 +20,7 @@
"type": "index-pattern",
"id": "be14ceb0-66b1-11e9-91c9-ffa52374d341",
"attributes": {
- "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}",
+ "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"fixed_interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}",
"title": "cloud_roll_index",
"type": "rollup"
},
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
index 2b2f8576d6769..b62bce700413a 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
@@ -37,7 +37,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
],
@@ -123,7 +123,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
],
@@ -174,7 +174,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
]
diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json
index 86a62b28abb5e..dab00a03b5468 100644
--- a/x-pack/plugins/ml/server/routes/apidoc.json
+++ b/x-pack/plugins/ml/server/routes/apidoc.json
@@ -20,6 +20,7 @@
"DataVisualizer",
"GetOverallStats",
"GetStatsForFields",
+ "GetHistogramsForFields",
"AnomalyDetectors",
"CreateAnomalyDetectors",
diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
index dea4803e8275e..7606420eacefc 100644
--- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
@@ -20,6 +20,7 @@ import {
import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns';
import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics';
import { getAuthorizationHeader } from '../lib/request_authorization';
+import { analyticsFeatureImportanceProvider } from '../models/data_frame_analytics/feature_importance';
function getIndexPatternId(context: RequestHandlerContext, patternName: string) {
const iph = new IndexPatternHandler(context.core.savedObjects.client);
@@ -545,4 +546,38 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
}
})
);
+
+ /**
+ * @apiGroup DataFrameAnalytics
+ *
+ * @api {get} /api/ml/data_frame/analytics/baseline Get analytics's feature importance baseline
+ * @apiName GetDataFrameAnalyticsBaseline
+ * @apiDescription Returns the baseline for data frame analytics job.
+ *
+ * @apiSchema (params) analyticsIdSchema
+ */
+ router.post(
+ {
+ path: '/api/ml/data_frame/analytics/{analyticsId}/baseline',
+ validate: {
+ params: analyticsIdSchema,
+ },
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
+ },
+ mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => {
+ try {
+ const { analyticsId } = request.params;
+ const { getRegressionAnalyticsBaseline } = analyticsFeatureImportanceProvider(client);
+ const baseline = await getRegressionAnalyticsBaseline(analyticsId);
+
+ return response.ok({
+ body: { baseline },
+ });
+ } catch (e) {
+ return response.customError(wrapError(e));
+ }
+ })
+ );
}
diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts
index a697fe017f192..50d9be1be4230 100644
--- a/x-pack/plugins/ml/server/routes/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts
@@ -84,7 +84,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
/**
* @apiGroup DataVisualizer
*
- * @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get histograms for fields
+ * @api {post} /api/ml/data_visualizer/get_field_histograms/:indexPatternTitle Get histograms for fields
* @apiName GetHistogramsForFields
* @apiDescription Returns the histograms on a list fields in the specified index pattern.
*
diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
index 24e45514e1efc..57bc5578e92c5 100644
--- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
@@ -32,8 +32,8 @@ export const dataVisualizerFieldStatsSchema = schema.object({
earliest: schema.maybe(schema.number()),
/** Latest timestamp for search, as epoch ms (optional). */
latest: schema.maybe(schema.number()),
- /** Aggregation interval to use for obtaining document counts over time (optional). */
- interval: schema.maybe(schema.string()),
+ /** Aggregation interval, in milliseconds, to use for obtaining document counts over time (optional). */
+ interval: schema.maybe(schema.number()),
/** Maximum number of examples to return for text type fields. */
maxExamples: schema.number(),
});
diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts
index 32b8691bd6049..2efc325a3edec 100644
--- a/x-pack/plugins/monitoring/server/config.test.ts
+++ b/x-pack/plugins/monitoring/server/config.test.ts
@@ -86,6 +86,9 @@ describe('config schema', () => {
"index": "filebeat-*",
},
"max_bucket_size": 10000,
+ "metricbeat": Object {
+ "index": "metricbeat-*",
+ },
"min_interval_seconds": 10,
"show_license_expiration": true,
},
diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts
index 789211c43db31..6ae99e3d16d64 100644
--- a/x-pack/plugins/monitoring/server/config.ts
+++ b/x-pack/plugins/monitoring/server/config.ts
@@ -29,6 +29,9 @@ export const configSchema = schema.object({
logs: schema.object({
index: schema.string({ defaultValue: 'filebeat-*' }),
}),
+ metricbeat: schema.object({
+ index: schema.string({ defaultValue: 'metricbeat-*' }),
+ }),
max_bucket_size: schema.number({ defaultValue: 10000 }),
elasticsearch: monitoringElasticsearchConfigSchema,
container: schema.object({
diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js
index dab1e87435c86..bef07124fb430 100644
--- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js
+++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js
@@ -5,6 +5,21 @@
*/
import { isFunction, get } from 'lodash';
+export function appendMetricbeatIndex(config, indexPattern) {
+ // Leverage this function to also append the dynamic metricbeat index too
+ let mbIndex = null;
+ // TODO: NP
+ // This function is called with both NP config and LP config
+ if (isFunction(config.get)) {
+ mbIndex = config.get('monitoring.ui.metricbeat.index');
+ } else {
+ mbIndex = get(config, 'monitoring.ui.metricbeat.index');
+ }
+
+ const newIndexPattern = `${indexPattern},${mbIndex}`;
+ return newIndexPattern;
+}
+
/**
* Prefix all comma separated index patterns within the original {@code indexPattern}.
*
@@ -27,7 +42,7 @@ export function prefixIndexPattern(config, indexPattern, ccs) {
}
if (!ccsEnabled || !ccs) {
- return indexPattern;
+ return appendMetricbeatIndex(config, indexPattern);
}
const patterns = indexPattern.split(',');
@@ -35,10 +50,10 @@ export function prefixIndexPattern(config, indexPattern, ccs) {
// if a wildcard is used, then we also want to search the local indices
if (ccs === '*') {
- return `${prefixedPattern},${indexPattern}`;
+ return appendMetricbeatIndex(config, `${prefixedPattern},${indexPattern}`);
}
- return prefixedPattern;
+ return appendMetricbeatIndex(config, prefixedPattern);
}
/**
diff --git a/x-pack/plugins/monitoring/server/lib/create_query.js b/x-pack/plugins/monitoring/server/lib/create_query.js
index 04e0d7642ec58..1983dc3dcf9af 100644
--- a/x-pack/plugins/monitoring/server/lib/create_query.js
+++ b/x-pack/plugins/monitoring/server/lib/create_query.js
@@ -57,7 +57,7 @@ export function createQuery(options) {
let typeFilter;
if (type) {
- typeFilter = { term: { type } };
+ typeFilter = { bool: { should: [{ term: { type } }, { term: { 'metricset.name': type } }] } };
}
let clusterUuidFilter;
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
index 6abb392e58818..84384021a3593 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
@@ -17,15 +17,23 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
return (response) => {
let nodeSummary = {};
const nodeStatsHits = get(response, 'hits.hits', []);
- const nodes = nodeStatsHits.map((hit) => hit._source.source_node); // using [0] value because query results are sorted desc per timestamp
+ const nodes = nodeStatsHits.map((hit) =>
+ get(hit, '_source.elasticsearch.node', hit._source.source_node)
+ ); // using [0] value because query results are sorted desc per timestamp
const node = nodes[0] || getDefaultNodeFromId(nodeUuid);
- const sourceStats = get(response, 'hits.hits[0]._source.node_stats');
+ const sourceStats =
+ get(response, 'hits.hits[0]._source.elasticsearch.node.stats') ||
+ get(response, 'hits.hits[0]._source.node_stats');
const clusterNode = get(clusterState, ['nodes', nodeUuid]);
const stats = {
resolver: nodeUuid,
- node_ids: nodes.map((node) => node.uuid),
+ node_ids: nodes.map((node) => node.id || node.uuid),
attributes: node.attributes,
- transport_address: node.transport_address,
+ transport_address: get(
+ response,
+ 'hits.hits[0]._source.service.address',
+ node.transport_address
+ ),
name: node.name,
type: node.type,
};
@@ -45,10 +53,17 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
totalShards: _shardStats.shardCount,
indexCount: _shardStats.indexCount,
documents: get(sourceStats, 'indices.docs.count'),
- dataSize: get(sourceStats, 'indices.store.size_in_bytes'),
- freeSpace: get(sourceStats, 'fs.total.available_in_bytes'),
- totalSpace: get(sourceStats, 'fs.total.total_in_bytes'),
- usedHeap: get(sourceStats, 'jvm.mem.heap_used_percent'),
+ dataSize:
+ get(sourceStats, 'indices.store.size_in_bytes') ||
+ get(sourceStats, 'indices.store.size.bytes'),
+ freeSpace:
+ get(sourceStats, 'fs.total.available_in_bytes') ||
+ get(sourceStats, 'fs.summary.available.bytes'),
+ totalSpace:
+ get(sourceStats, 'fs.total.total_in_bytes') || get(sourceStats, 'fs.summary.total.bytes'),
+ usedHeap:
+ get(sourceStats, 'jvm.mem.heap_used_percent') ||
+ get(sourceStats, 'jvm.mem.heap.used.pct'),
status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', {
defaultMessage: 'Online',
}),
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
index 573f1792e5f8a..68bca96e2911b 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
@@ -19,6 +19,7 @@ export async function getNodeIds(req, indexPattern, { clusterUuid }, size) {
filterPath: ['aggregations.composite_data.buckets'],
body: {
query: createQuery({
+ type: 'node_stats',
start,
end,
metric: ElasticsearchMetric.getMetricFields(),
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
index 682da324ee72f..c2794b7e7fa44 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
@@ -96,6 +96,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n
},
filterPath: [
'hits.hits._source.source_node',
+ 'hits.hits._source.elasticsearch.node',
'aggregations.nodes.buckets.key',
...LISTING_METRICS_PATHS,
],
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
index 3c719c2ddfbf8..317c1cddf57ae 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
@@ -17,25 +17,29 @@ export function mapNodesInfo(nodeHits, clusterStats, nodesShardCount) {
const clusterState = get(clusterStats, 'cluster_state', { nodes: {} });
return nodeHits.reduce((prev, node) => {
- const sourceNode = get(node, '_source.source_node');
+ const sourceNode = get(node, '_source.source_node') || get(node, '_source.elasticsearch.node');
const calculatedNodeType = calculateNodeType(sourceNode, get(clusterState, 'master_node'));
const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(
sourceNode,
calculatedNodeType
);
- const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid]));
+ const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid || sourceNode.id]));
return {
...prev,
- [sourceNode.uuid]: {
+ [sourceNode.uuid || sourceNode.id]: {
name: sourceNode.name,
transport_address: sourceNode.transport_address,
type: nodeType,
isOnline,
nodeTypeLabel: nodeTypeLabel,
nodeTypeClass: nodeTypeClass,
- shardCount: get(nodesShardCount, `nodes[${sourceNode.uuid}].shardCount`, 0),
+ shardCount: get(
+ nodesShardCount,
+ `nodes[${sourceNode.uuid || sourceNode.id}].shardCount`,
+ 0
+ ),
},
};
}, {});
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
index 636082656f1a4..5e9c1818cad2b 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
@@ -74,7 +74,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
]
`);
@@ -111,7 +111,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/delete",
@@ -158,7 +158,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/delete",
@@ -172,7 +172,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find",
]
`);
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
index 540b9e5c1e56e..eb278a5755204 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
@@ -8,7 +8,7 @@ import { uniq } from 'lodash';
import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server';
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
-const readOperations: string[] = ['get', 'getAlertState', 'getAlertStatus', 'find'];
+const readOperations: string[] = ['get', 'getAlertState', 'getAlertInstanceSummary', 'find'];
const writeOperations: string[] = [
'create',
'delete',
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index 366bf7a1df1f2..a6018837fa4fe 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -7,6 +7,7 @@
export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
+export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current-*';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
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 0955f196df176..e1ff34463d215 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -1132,7 +1132,8 @@ export class EndpointDocGenerator {
path: '/package/endpoint/0.5.0',
icons: [
{
- src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
+ path: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
+ src: '/img/logo-endpoint-64-color.svg',
size: '16x16',
type: 'image/svg+xml',
},
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index 8e507cbc921a2..e0bd916103a28 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -445,6 +445,13 @@ export type HostInfo = Immutable<{
host_status: HostStatus;
}>;
+export type HostMetadataDetails = Immutable<{
+ agent: {
+ id: string;
+ };
+ HostDetails: HostMetadata;
+}>;
+
export type HostMetadata = Immutable<{
'@timestamp': number;
event: {
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 b7d905d22e839..35fcc3b07fd05 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
@@ -23,6 +23,8 @@ import {
} from './hosts';
import {
NetworkQueries,
+ NetworkDetailsStrategyResponse,
+ NetworkDetailsRequestOptions,
NetworkDnsStrategyResponse,
NetworkDnsRequestOptions,
NetworkTlsStrategyResponse,
@@ -35,6 +37,8 @@ import {
NetworkTopCountriesRequestOptions,
NetworkTopNFlowStrategyResponse,
NetworkTopNFlowRequestOptions,
+ NetworkUsersStrategyResponse,
+ NetworkUsersRequestOptions,
} from './network';
import {
MatrixHistogramQuery,
@@ -87,6 +91,8 @@ export type StrategyResponseType = T extends HostsQ
? HostFirstLastSeenStrategyResponse
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesStrategyResponse
+ : T extends NetworkQueries.details
+ ? NetworkDetailsStrategyResponse
: T extends NetworkQueries.dns
? NetworkDnsStrategyResponse
: T extends NetworkQueries.http
@@ -99,6 +105,8 @@ export type StrategyResponseType = T extends HostsQ
? NetworkTopCountriesStrategyResponse
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowStrategyResponse
+ : T extends NetworkQueries.users
+ ? NetworkUsersStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
: never;
@@ -115,6 +123,8 @@ export type StrategyRequestType = T extends HostsQu
? HostFirstLastSeenRequestOptions
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesRequestOptions
+ : T extends NetworkQueries.details
+ ? NetworkDetailsRequestOptions
: T extends NetworkQueries.dns
? NetworkDnsRequestOptions
: T extends NetworkQueries.http
@@ -127,6 +137,8 @@ export type StrategyRequestType = T extends HostsQu
? NetworkTopCountriesRequestOptions
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowRequestOptions
+ : T extends NetworkQueries.users
+ ? NetworkUsersRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
: never;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
index 66676569b3c9e..19521741c5f66 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
@@ -15,6 +15,13 @@ export enum NetworkTopTablesFields {
source_ips = 'source_ips',
}
+export enum FlowTarget {
+ client = 'client',
+ destination = 'destination',
+ server = 'server',
+ source = 'source',
+}
+
export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
new file mode 100644
index 0000000000000..920d7cf8c5eaf
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { HostEcs } from '../../../../ecs/host';
+import { GeoEcs } from '../../../../ecs/geo';
+import { Inspect, Maybe, TotalValue, Hit, ShardsResponse } from '../../../common';
+import { RequestBasicOptions } from '../..';
+
+export interface NetworkDetailsRequestOptions extends Omit {
+ ip: string;
+}
+
+export interface NetworkDetailsStrategyResponse extends IEsSearchResponse {
+ networkDetails: {
+ client?: Maybe;
+ destination?: Maybe;
+ host?: HostEcs;
+ server?: Maybe;
+ source?: Maybe;
+ };
+ inspect?: Maybe;
+}
+
+export interface NetworkDetails {
+ firstSeen?: Maybe;
+ lastSeen?: Maybe;
+ autonomousSystem: AutonomousSystem;
+ geo: GeoEcs;
+}
+
+export interface AutonomousSystem {
+ number?: Maybe;
+ organization?: Maybe;
+}
+
+export interface AutonomousSystemOrganization {
+ name?: Maybe;
+}
+
+interface ResultHit {
+ doc_count: number;
+ results: {
+ hits: {
+ total: TotalValue | number;
+ max_score: number | null;
+ hits: Array<{
+ _source: T;
+ sort?: [number];
+ _index?: string;
+ _type?: string;
+ _id?: string;
+ _score?: number | null;
+ }>;
+ };
+ };
+}
+
+export interface NetworkHit {
+ took?: number;
+ timed_out?: boolean;
+ _scroll_id?: string;
+ _shards?: ShardsResponse;
+ timeout?: number;
+ hits?: {
+ total: number;
+ hits: Hit[];
+ };
+ doc_count: number;
+ geo: ResultHit;
+ autonomousSystem: ResultHit;
+ firstSeen: {
+ value: number;
+ value_as_string: string;
+ };
+ lastSeen: {
+ value: number;
+ value_as_string: string;
+ };
+}
+
+export type NetworkDetailsHostHit = ResultHit;
+
+export interface NetworkDetailsHit {
+ aggregations: {
+ destination?: NetworkHit;
+ source?: NetworkHit;
+ host: ResultHit;
+ };
+ _shards: {
+ total: number;
+ successful: number;
+ skipped: number;
+ failed: number;
+ };
+ hits: {
+ total: {
+ value: number;
+ relation: string;
+ };
+ max_score: number | null;
+ hits: [];
+ };
+ took: number;
+ timeout: number;
+}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts
index ad58442b16994..cd661cd9b9e9f 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts
@@ -8,6 +8,16 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/comm
import { Maybe, CursorType, Inspect, PageInfoPaginated, GenericBuckets } from '../../../common';
import { RequestOptionsPaginated } from '../..';
+export enum NetworkHttpFields {
+ domains = 'domains',
+ lastHost = 'lastHost',
+ lastSourceIp = 'lastSourceIp',
+ methods = 'methods',
+ path = 'path',
+ requestCount = 'requestCount',
+ statuses = 'statuses',
+}
+
export interface NetworkHttpRequestOptions extends RequestOptionsPaginated {
ip?: string;
defaultIndex: string[];
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
index d61acbe62ffb0..4e73fe11ef430 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
@@ -5,18 +5,22 @@
*/
export * from './common';
+export * from './details';
export * from './dns';
export * from './http';
export * from './overview';
export * from './tls';
export * from './top_countries';
export * from './top_n_flow';
+export * from './users';
export enum NetworkQueries {
+ details = 'networkDetails',
dns = 'dns',
http = 'http',
overview = 'overviewNetwork',
tls = 'tls',
topCountries = 'topCountries',
topNFlow = 'topNFlow',
+ users = 'users',
}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts
index dffc994fcf4cb..5e1c9459aaac3 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts
@@ -9,7 +9,7 @@ import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common';
import { RequestOptionsPaginated } from '../..';
import { FlowTargetSourceDest } from '../common';
-export interface TlsBuckets {
+export interface NetworkTlsBuckets {
key: string;
timestamp?: {
value: number;
@@ -29,7 +29,7 @@ export interface TlsBuckets {
};
}
-export interface TlsNode {
+export interface NetworkTlsNode {
_id?: Maybe;
timestamp?: Maybe;
notAfter?: Maybe;
@@ -38,23 +38,23 @@ export interface TlsNode {
issuers?: Maybe;
}
-export enum TlsFields {
+export enum NetworkTlsFields {
_id = '_id',
}
-export interface TlsEdges {
- node: TlsNode;
+export interface NetworkTlsEdges {
+ node: NetworkTlsNode;
cursor: CursorType;
}
-export interface NetworkTlsRequestOptions extends RequestOptionsPaginated {
+export interface NetworkTlsRequestOptions extends RequestOptionsPaginated {
ip: string;
flowTarget: FlowTargetSourceDest;
defaultIndex: string[];
}
export interface NetworkTlsStrategyResponse extends IEsSearchResponse {
- edges: TlsEdges[];
+ edges: NetworkTlsEdges[];
totalCount: number;
pageInfo: PageInfoPaginated;
inspect?: Maybe;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
index a28388a2c6f8f..2c89bbf048e62 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
@@ -14,13 +14,6 @@ import {
TopNetworkTablesEcsField,
} from '../common';
-export enum FlowTarget {
- client = 'client',
- destination = 'destination',
- server = 'server',
- source = 'source',
-}
-
export interface TopCountriesItemSource {
country?: Maybe;
destination_ips?: Maybe;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts
new file mode 100644
index 0000000000000..196317e7587bf
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { CursorType, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common';
+import { FlowTarget } from '../common';
+import { RequestOptionsPaginated } from '../..';
+
+export enum NetworkUsersFields {
+ name = 'name',
+ count = 'count',
+}
+
+export interface NetworkUsersRequestOptions extends RequestOptionsPaginated {
+ ip: string;
+ sort: SortField;
+ flowTarget: FlowTarget;
+}
+
+export interface NetworkUsersStrategyResponse extends IEsSearchResponse {
+ edges: NetworkUsersEdges[];
+ totalCount: number;
+ pageInfo: PageInfoPaginated;
+ inspect?: Maybe;
+}
+
+export interface NetworkUsersEdges {
+ node: NetworkUsersNode;
+ cursor: CursorType;
+}
+
+export interface NetworkUsersNode {
+ _id?: Maybe;
+ timestamp?: Maybe;
+ user?: Maybe;
+}
+
+export interface NetworkUsersItem {
+ name?: Maybe;
+ id?: Maybe;
+ groupId?: Maybe;
+ groupName?: Maybe;
+ count?: Maybe;
+}
+
+export interface NetworkUsersBucketsItem {
+ key: string;
+ doc_count: number;
+ groupName?: NetworkUsersGroupName;
+ groupId?: NetworkUsersGroupId;
+ id?: Id;
+}
+
+export interface NetworkUsersGroupName {
+ doc_count_error_upper_bound: number;
+ sum_other_doc_count: number;
+ buckets: NetworkUsersBucketsItem[];
+}
+
+export interface NetworkUsersGroupId {
+ doc_count_error_upper_bound: number;
+ sum_other_doc_count: number;
+ buckets: NetworkUsersBucketsItem[];
+}
+
+interface Id {
+ doc_count_error_upper_bound: number;
+ sum_other_doc_count: number;
+ buckets: NetworkUsersBucketsItem[];
+}
diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
index d55a8faae021d..5b42897b065e3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
@@ -18,8 +18,7 @@ const ABSOLUTE_DATE = {
startTime: '2019-08-01T20:03:29.186Z',
};
-// FLAKY: https://github.com/elastic/kibana/issues/75697
-describe.skip('URL compatibility', () => {
+describe('URL compatibility', () => {
it('Redirects to Detection alerts from old Detections URL', () => {
loginAndWaitForPage(DETECTIONS);
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts
index 6c9620e27fabf..07855c3477106 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts
@@ -9,7 +9,7 @@ import { SecurityPageName } from '../../../../app/types';
export { getDetectionEngineUrl } from '../redirect_to_detection_engine';
export { getAppOverviewUrl } from '../redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts';
-export { getNetworkUrl, getIPDetailsUrl } from '../redirect_to_network';
+export { getNetworkUrl, getNetworkDetailsUrl } from '../redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl } from '../redirect_to_timelines';
export {
getCaseDetailsUrl,
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
index c6e58d4206958..403c8d838fa44 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
@@ -15,7 +15,7 @@ import { useKibana } from '../../lib/kibana';
export { getDetectionEngineUrl } from './redirect_to_detection_engine';
export { getAppOverviewUrl } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
-export { getNetworkUrl, getIPDetailsUrl } from './redirect_to_network';
+export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines';
export {
getCaseDetailsUrl,
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx
index 100c5e46141a2..c042c8e1470b4 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx
@@ -13,7 +13,7 @@ import { appendSearch } from './helpers';
export const getNetworkUrl = (search?: string) => `${appendSearch(search)}`;
-export const getIPDetailsUrl = (
+export const getNetworkDetailsUrl = (
detailName: string,
flowTarget?: FlowTarget | FlowTargetSourceDest,
search?: string
diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx
index b6817c4cab1f2..e6d34b5e432ac 100644
--- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx
@@ -14,7 +14,7 @@ import { useUiSetting$ } from '../../lib/kibana';
import {
GoogleLink,
HostDetailsLink,
- IPDetailsLink,
+ NetworkDetailsLink,
ReputationLink,
WhoIsLink,
CertificateFingerprintLink,
@@ -61,9 +61,9 @@ describe('Custom Links', () => {
});
});
- describe('IPDetailsLink', () => {
+ describe('NetworkDetailsLink', () => {
test('should render valid link to IP Details with ipv4 as the display text', () => {
- const wrapper = mount();
+ const wrapper = mount();
expect(wrapper.find('EuiLink').prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4)}/source`
);
@@ -71,7 +71,7 @@ describe('Custom Links', () => {
});
test('should render valid link to IP Details with child text as the display text', () => {
- const wrapper = mount({hostName});
+ const wrapper = mount({hostName});
expect(wrapper.find('EuiLink').prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4)}/source`
);
@@ -79,7 +79,7 @@ describe('Custom Links', () => {
});
test('should render valid link to IP Details with ipv6 as the display text', () => {
- const wrapper = mount();
+ const wrapper = mount();
expect(wrapper.find('EuiLink').prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv6Encoded)}/source`
);
diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx
index 943f2d8336ca7..d6cbd31e86ddb 100644
--- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx
@@ -28,7 +28,7 @@ import { encodeIpv6 } from '../../lib/helpers';
import {
getCaseDetailsUrl,
getHostDetailsUrl,
- getIPDetailsUrl,
+ getNetworkDetailsUrl,
getCreateCaseUrl,
useFormatUrl,
} from '../link_to';
@@ -114,7 +114,7 @@ export const ExternalLink = React.memo<{
ExternalLink.displayName = 'ExternalLink';
-const IPDetailsLinkComponent: React.FC<{
+const NetworkDetailsLinkComponent: React.FC<{
children?: React.ReactNode;
ip: string;
flowTarget?: FlowTarget | FlowTargetSourceDest;
@@ -125,7 +125,7 @@ const IPDetailsLinkComponent: React.FC<{
(ev) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${SecurityPageName.network}`, {
- path: getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search),
+ path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search),
});
},
[flowTarget, ip, navigateToApp, search]
@@ -134,14 +134,14 @@ const IPDetailsLinkComponent: React.FC<{
return (
{children ? children : ip}
);
};
-export const IPDetailsLink = React.memo(IPDetailsLinkComponent);
+export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);
const CaseDetailsLinkComponent: React.FC<{
children?: React.ReactNode;
diff --git a/x-pack/plugins/security_solution/public/common/components/links/translations.ts b/x-pack/plugins/security_solution/public/common/components/links/translations.ts
index cf7db8bfa8161..014acdf3fd557 100644
--- a/x-pack/plugins/security_solution/public/common/components/links/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/links/translations.ts
@@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
-export * from '../../../network/components/ip_overview/translations';
+export * from '../../../network/components/details/translations';
export const CASE_DETAILS_LINK_ARIA = (detailName: string) =>
i18n.translate('xpack.securitySolution.case.caseTable.caseDetailsLinkAria', {
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
index 52b26a20a8f64..3dd408e5aa822 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
@@ -14,7 +14,7 @@ import { Anomaly, AnomaliesByNetwork } from '../types';
import { getRowItemDraggable } from '../../tables/helpers';
import { EntityDraggable } from '../entity_draggable';
import { createCompoundNetworkKey } from './create_compound_key';
-import { IPDetailsLink } from '../../links';
+import { NetworkDetailsLink } from '../../links';
import * as i18n from './translations';
import { getEntries } from '../get_entries';
@@ -46,7 +46,7 @@ export const getAnomaliesNetworkTableColumns = (
rowItem: ip,
attrName: anomaliesByNetwork.type,
idPrefix: `anomalies-network-table-ip-${createCompoundNetworkKey(anomaliesByNetwork)}`,
- render: (item) => ,
+ render: (item) => ,
}),
},
{
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
index a10e4cf568dd1..2964572cb7cf3 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
@@ -10,7 +10,7 @@ import { ChromeBreadcrumb } from '../../../../../../../../src/core/public';
import { APP_NAME } from '../../../../../common/constants';
import { StartServices } from '../../../../types';
import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils';
-import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details';
+import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details';
import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils';
import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils';
import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages';
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
index 448fa58443407..f7b69c4fc8ed3 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
@@ -16,12 +16,12 @@ import {
EuiLoadingContent,
EuiPagination,
EuiPopover,
- Direction,
} from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { FC, memo, useState, useEffect, ComponentType } from 'react';
import styled from 'styled-components';
+import { Direction } from '../../../../common/search_strategy';
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants';
import { AuthTableColumns } from '../../../hosts/components/authentications_table';
import { HostsTableColumns } from '../../../hosts/components/hosts_table';
@@ -29,11 +29,11 @@ import { NetworkDnsColumns } from '../../../network/components/network_dns_table
import { NetworkHttpColumns } from '../../../network/components/network_http_table/columns';
import {
NetworkTopNFlowColumns,
- NetworkTopNFlowColumnsIpDetails,
+ NetworkTopNFlowColumnsNetworkDetails,
} from '../../../network/components/network_top_n_flow_table/columns';
import {
NetworkTopCountriesColumns,
- NetworkTopCountriesColumnsIpDetails,
+ NetworkTopCountriesColumnsNetworkDetails,
} from '../../../network/components/network_top_countries_table/columns';
import { TlsColumns } from '../../../network/components/tls_table/columns';
import { UncommonProcessTableColumns } from '../../../hosts/components/uncommon_process_table';
@@ -78,9 +78,9 @@ declare type BasicTableColumns =
| NetworkDnsColumns
| NetworkHttpColumns
| NetworkTopCountriesColumns
- | NetworkTopCountriesColumnsIpDetails
+ | NetworkTopCountriesColumnsNetworkDetails
| NetworkTopNFlowColumns
- | NetworkTopNFlowColumnsIpDetails
+ | NetworkTopNFlowColumnsNetworkDetails
| TlsColumns
| UncommonProcessTableColumns
| UsersColumns;
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
index 833f85712b5fa..9e6982ea20301 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
@@ -4,14 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common';
-import { connector as jiraConnectorConfig } from './jira/config';
+/* eslint-disable @kbn/eslint/no-restricted-paths */
+
+import {
+ ServiceNowConnectorConfiguration,
+ JiraConnectorConfiguration,
+} from '../../../../../triggers_actions_ui/public/common';
import { connector as resilientConnectorConfig } from './resilient/config';
import { ConnectorConfiguration } from './types';
export const connectorsConfiguration: Record = {
'.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration,
- '.jira': jiraConnectorConfig,
+ '.jira': JiraConnectorConfiguration as ConnectorConfiguration,
'.resilient': resilientConnectorConfig,
};
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
index f32e1e0df184e..33afa82c84f34 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
@@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { getActionType as jiraActionType } from './jira';
export { getActionType as resilientActionType } from './resilient';
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx
deleted file mode 100644
index 0737db3cd08eb..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import {
- EuiFieldText,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiFieldPassword,
- EuiSpacer,
-} from '@elastic/eui';
-
-import * as i18n from './translations';
-import { ConnectorFlyoutFormProps } from '../types';
-import { JiraActionConnector } from './types';
-import { withConnectorFlyout } from '../components/connector_flyout';
-
-const JiraConnectorForm: React.FC> = ({
- errors,
- action,
- onChangeSecret,
- onBlurSecret,
- onChangeConfig,
- onBlurConfig,
-}) => {
- const { projectKey } = action.config;
- const { email, apiToken } = action.secrets;
- const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null;
- const isEmailInvalid: boolean = errors.email.length > 0 && email != null;
- const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null;
-
- return (
- <>
-
-
-
- onChangeConfig('projectKey', evt.target.value)}
- onBlur={() => onBlurConfig('projectKey')}
- />
-
-
-
-
-
-
-
- onChangeSecret('email', evt.target.value)}
- onBlur={() => onBlurSecret('email')}
- />
-
-
-
-
-
-
-
- onChangeSecret('apiToken', evt.target.value)}
- onBlur={() => onBlurSecret('apiToken')}
- />
-
-
-
- >
- );
-};
-
-export const JiraConnectorFlyout = withConnectorFlyout({
- ConnectorFormComponent: JiraConnectorForm,
- secretKeys: ['email', 'apiToken'],
- configKeys: ['projectKey'],
- connectorActionTypeId: '.jira',
-});
-
-// eslint-disable-next-line import/no-default-export
-export { JiraConnectorFlyout as default };
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx
deleted file mode 100644
index cead392010dc7..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { lazy } from 'react';
-import {
- ValidationResult,
- // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../../../../../triggers_actions_ui/public/types';
-
-import { connector } from './config';
-import { createActionType } from '../utils';
-import logo from './logo.svg';
-import { JiraActionConnector } from './types';
-import * as i18n from './translations';
-
-interface Errors {
- projectKey: string[];
- email: string[];
- apiToken: string[];
-}
-
-const validateConnector = (action: JiraActionConnector): ValidationResult => {
- const errors: Errors = {
- projectKey: [],
- email: [],
- apiToken: [],
- };
-
- if (!action.config.projectKey) {
- errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED];
- }
-
- if (!action.secrets.email) {
- errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED];
- }
-
- if (!action.secrets.apiToken) {
- errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED];
- }
-
- return { errors };
-};
-
-export const getActionType = createActionType({
- id: connector.id,
- iconClass: logo,
- selectMessage: i18n.JIRA_DESC,
- actionTypeTitle: connector.name,
- validateConnector,
- actionConnectorFields: lazy(() => import('./flyout')),
-});
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts
deleted file mode 100644
index d7abf77a58d4c..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export * from '../translations';
-
-export const JIRA_DESC = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.selectMessageText',
- {
- defaultMessage: 'Push or update Security case data to a new issue in Jira',
- }
-);
-
-export const JIRA_TITLE = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.actionTypeTitle',
- {
- defaultMessage: 'Jira',
- }
-);
-
-export const JIRA_PROJECT_KEY_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.projectKey',
- {
- defaultMessage: 'Project key',
- }
-);
-
-export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField',
- {
- defaultMessage: 'Project key is required',
- }
-);
-
-export const JIRA_EMAIL_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.emailTextFieldLabel',
- {
- defaultMessage: 'Email or Username',
- }
-);
-
-export const JIRA_EMAIL_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredEmailTextField',
- {
- defaultMessage: 'Email or Username is required',
- }
-);
-
-export const JIRA_API_TOKEN_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel',
- {
- defaultMessage: 'API token or Password',
- }
-);
-
-export const JIRA_API_TOKEN_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField',
- {
- defaultMessage: 'API token or Password is required',
- }
-);
-
-export const MAPPING_FIELD_SUMMARY = i18n.translate(
- 'xpack.securitySolution.case.configureCases.mappingFieldSummary',
- {
- defaultMessage: 'Summary',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts
deleted file mode 100644
index fafb4a0d41fb3..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-/* eslint-disable no-restricted-imports */
-/* eslint-disable @kbn/eslint/no-restricted-paths */
-
-import {
- JiraPublicConfigurationType,
- JiraSecretConfigurationType,
-} from '../../../../../../actions/server/builtin_action_types/jira/types';
-
-export { JiraFieldsType } from '../../../../../../case/common/api/connectors';
-
-export * from '../types';
-
-export interface JiraActionConnector {
- config: JiraPublicConfigurationType;
- secrets: JiraSecretConfigurationType;
-}
diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
index 2849e8ffabd36..a74c9a6d2009d 100644
--- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
@@ -11,9 +11,9 @@ import {
HostsFields,
NetworkDnsFields,
NetworkTopTablesFields,
- TlsFields,
- UsersFields,
-} from '../../graphql/types';
+ NetworkTlsFields,
+ NetworkUsersFields,
+} from '../../../common/search_strategy';
import { State } from '../store';
import { defaultHeaders } from './header';
@@ -100,7 +100,7 @@ export const mockGlobalState: State = {
[networkModel.NetworkTableType.tls]: {
activePage: 0,
limit: 10,
- sort: { field: TlsFields._id, direction: Direction.desc },
+ sort: { field: NetworkTlsFields._id, direction: Direction.desc },
},
[networkModel.NetworkTableType.http]: {
activePage: 0,
@@ -116,37 +116,37 @@ export const mockGlobalState: State = {
details: {
flowTarget: FlowTarget.source,
queries: {
- [networkModel.IpDetailsTableType.topCountriesDestination]: {
+ [networkModel.NetworkDetailsTableType.topCountriesDestination]: {
activePage: 0,
limit: 10,
sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc },
},
- [networkModel.IpDetailsTableType.topCountriesSource]: {
+ [networkModel.NetworkDetailsTableType.topCountriesSource]: {
activePage: 0,
limit: 10,
sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc },
},
- [networkModel.IpDetailsTableType.topNFlowSource]: {
+ [networkModel.NetworkDetailsTableType.topNFlowSource]: {
activePage: 0,
limit: 10,
sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc },
},
- [networkModel.IpDetailsTableType.topNFlowDestination]: {
+ [networkModel.NetworkDetailsTableType.topNFlowDestination]: {
activePage: 0,
limit: 10,
sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc },
},
- [networkModel.IpDetailsTableType.tls]: {
+ [networkModel.NetworkDetailsTableType.tls]: {
activePage: 0,
limit: 10,
- sort: { field: TlsFields._id, direction: Direction.desc },
+ sort: { field: NetworkTlsFields._id, direction: Direction.desc },
},
- [networkModel.IpDetailsTableType.users]: {
+ [networkModel.NetworkDetailsTableType.users]: {
activePage: 0,
limit: 10,
- sort: { field: UsersFields.name, direction: Direction.asc },
+ sort: { field: NetworkUsersFields.name, direction: Direction.asc },
},
- [networkModel.IpDetailsTableType.http]: {
+ [networkModel.NetworkDetailsTableType.http]: {
activePage: 0,
limit: 10,
sort: { direction: Direction.desc },
diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
index c7810af13eb74..5a4eaad72ac32 100644
--- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
@@ -95,6 +95,7 @@ export const useFormFieldMock = (options?: Partial): FieldHook => {
clearErrors: jest.fn(),
validate: jest.fn(),
reset: jest.fn(),
+ __isIncludedInOutput: true,
__serializeValue: jest.fn(),
...options,
};
diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts
index cd8836e38bfef..6b446ab6692d9 100644
--- a/x-pack/plugins/security_solution/public/common/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/common/store/actions.ts
@@ -7,10 +7,16 @@
import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action';
import { PolicyListAction } from '../../management/pages/policy/store/policy_list';
import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';
+import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action';
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';
export { inputsActions } from './inputs';
import { RoutingAction } from './routing';
-export type AppAction = EndpointAction | RoutingAction | PolicyListAction | PolicyDetailsAction;
+export type AppAction =
+ | EndpointAction
+ | RoutingAction
+ | PolicyListAction
+ | PolicyDetailsAction
+ | TrustedAppsPageAction;
diff --git a/x-pack/plugins/security_solution/public/common/store/routing/action.ts b/x-pack/plugins/security_solution/public/common/store/routing/action.ts
index ae5e4eb32d476..d0cc38970ca21 100644
--- a/x-pack/plugins/security_solution/public/common/store/routing/action.ts
+++ b/x-pack/plugins/security_solution/public/common/store/routing/action.ts
@@ -6,7 +6,7 @@
import { AppLocation, Immutable } from '../../../../common/endpoint/types';
-interface UserChangedUrl {
+export interface UserChangedUrl {
readonly type: 'userChangedUrl';
readonly payload: Immutable;
}
diff --git a/x-pack/plugins/security_solution/public/helpers.test.ts b/x-pack/plugins/security_solution/public/helpers.test.ts
new file mode 100644
index 0000000000000..9244452a23e6d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/helpers.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { parseRoute } from './helpers';
+
+describe('public helpers parseRoute', () => {
+ it('should properly parse hash route', () => {
+ const hashSearch =
+ '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))';
+ const hashLocation = {
+ hash: `#/detections/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${hashSearch}`,
+ pathname: '/app/siem',
+ search: '',
+ };
+
+ expect(parseRoute(hashLocation)).toEqual({
+ pageName: 'detections',
+ path: `/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${hashSearch}`,
+ search: hashSearch,
+ });
+ });
+
+ it('should properly parse non-hash route', () => {
+ const nonHashLocation = {
+ hash: '',
+ pathname: '/app/security/detections/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit',
+ search:
+ '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))',
+ };
+
+ expect(parseRoute(nonHashLocation)).toEqual({
+ pageName: 'detections',
+ path: `/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${nonHashLocation.search}`,
+ search: nonHashLocation.search,
+ });
+ });
+
+ it('should properly parse non-hash subplugin route', () => {
+ const nonHashLocation = {
+ hash: '',
+ pathname: '/app/security/detections',
+ search:
+ '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))',
+ };
+
+ expect(parseRoute(nonHashLocation)).toEqual({
+ pageName: 'detections',
+ path: `${nonHashLocation.search}`,
+ search: nonHashLocation.search,
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts
index 63c3f3ea81d98..92f3d23907559 100644
--- a/x-pack/plugins/security_solution/public/helpers.ts
+++ b/x-pack/plugins/security_solution/public/helpers.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEmpty } from 'lodash/fp';
+
import { CoreStart } from '../../../../src/core/public';
import { APP_ID } from '../common/constants';
import {
@@ -13,13 +15,37 @@ import {
import { SecurityPageName } from './app/types';
import { InspectResponse } from './types';
+export const parseRoute = (location: Pick) => {
+ if (!isEmpty(location.hash)) {
+ const hashPath = location.hash.split('?');
+ const search = hashPath.length >= 1 ? `?${hashPath[1]}` : '';
+ const pageRoute = hashPath.length > 0 ? hashPath[0].split('/') : [];
+ const pageName = pageRoute.length >= 1 ? pageRoute[1] : '';
+ const path = `/${pageRoute.slice(2).join('/') ?? ''}${search}`;
+
+ return {
+ pageName,
+ path,
+ search,
+ };
+ }
+
+ const search = location.search;
+ const pageRoute = location.pathname.split('/');
+ const pageName = pageRoute[3];
+ const subpluginPath = pageRoute.length > 4 ? `/${pageRoute.slice(4).join('/')}` : '';
+ const path = `${subpluginPath}${search}`;
+
+ return {
+ pageName,
+ path,
+ search,
+ };
+};
+
export const manageOldSiemRoutes = async (coreStart: CoreStart) => {
const { application } = coreStart;
- const hashPath = window.location.hash.split('?');
- const search = hashPath.length >= 1 ? hashPath[1] : '';
- const pageRoute = hashPath.length > 0 ? hashPath[0].split('/') : [];
- const pageName = pageRoute.length >= 1 ? pageRoute[1] : '';
- const path = `/${pageRoute.slice(2).join('/') ?? ''}?${search}`;
+ const { pageName, path, search } = parseRoute(window.location);
switch (pageName) {
case SecurityPageName.overview:
@@ -73,7 +99,7 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => {
default:
application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, {
replace: true,
- path: `?${search}`,
+ path: `${search}`,
});
break;
}
diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
index 8e2b47769adf3..3d291d9bf7b28 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
@@ -20,7 +20,7 @@ import {
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
-import { HostDetailsLink, IPDetailsLink } from '../../../common/components/links';
+import { HostDetailsLink, NetworkDetailsLink } from '../../../common/components/links';
import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
import { Provider } from '../../../timelines/components/timeline/data_providers/provider';
@@ -264,7 +264,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [
: null,
attrName: 'source.ip',
idPrefix: `authentications-table-${node._id}-lastSuccessSource`,
- render: (item) => ,
+ render: (item) => ,
}),
},
{
@@ -309,7 +309,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [
: null,
attrName: 'source.ip',
idPrefix: `authentications-table-${node._id}-lastFailureSource`,
- render: (item) => ,
+ render: (item) => ,
}),
},
{
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
index 49b63a5f76a14..d8cd59f119d52 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
@@ -9,6 +9,7 @@ import { noop } from 'lodash/fp';
import React, { useEffect, useCallback, useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
+import { HostItem } from '../../../../common/search_strategy';
import { SecurityPageName } from '../../../app/types';
import { UpdateDateRange } from '../../../common/components/charts/common';
import { FiltersGlobal } from '../../../common/components/filters_global';
@@ -137,7 +138,7 @@ const HostDetailsComponent = React.memo(
inspect={inspect}
refetch={refetch}
setQuery={setQuery}
- data={hostOverview}
+ data={hostOverview as HostItem}
anomaliesData={anomaliesData}
isLoadingAnomaliesData={isLoadingAnomaliesData}
loading={loading}
diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts
index 06f0f09bcf54d..cd4ce743bb701 100644
--- a/x-pack/plugins/security_solution/public/management/common/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/common/constants.ts
@@ -24,6 +24,12 @@ export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList';
export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails';
/** Namespace within the Management state where endpoint-host state is maintained */
export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';
+/** Namespace within the Management state where trusted apps page state is maintained */
+export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps';
+
+export const MANAGEMENT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50];
+export const MANAGEMENT_DEFAULT_PAGE = 0;
+export const MANAGEMENT_DEFAULT_PAGE_SIZE = 10;
// --[ DEFAULTS ]---------------------------------------------------------------------------
/** The default polling interval to start all polling pages */
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts
new file mode 100644
index 0000000000000..7a36654dcffc3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/common/routing.test.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { extractListPaginationParams, getTrustedAppsListPath } from './routing';
+import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from './constants';
+
+describe('routing', () => {
+ describe('extractListPaginationParams()', () => {
+ it('extracts default page index when not provided', () => {
+ expect(extractListPaginationParams({}).page_index).toBe(MANAGEMENT_DEFAULT_PAGE);
+ });
+
+ it('extracts default page index when too small value provided', () => {
+ expect(extractListPaginationParams({ page_index: '-1' }).page_index).toBe(
+ MANAGEMENT_DEFAULT_PAGE
+ );
+ });
+
+ it('extracts default page index when not a number provided', () => {
+ expect(extractListPaginationParams({ page_index: 'a' }).page_index).toBe(
+ MANAGEMENT_DEFAULT_PAGE
+ );
+ });
+
+ it('extracts only last page index when multiple values provided', () => {
+ expect(extractListPaginationParams({ page_index: ['1', '2'] }).page_index).toBe(2);
+ });
+
+ it('extracts proper page index when single valid value provided', () => {
+ expect(extractListPaginationParams({ page_index: '2' }).page_index).toBe(2);
+ });
+
+ it('extracts default page size when not provided', () => {
+ expect(extractListPaginationParams({}).page_size).toBe(MANAGEMENT_DEFAULT_PAGE_SIZE);
+ });
+
+ it('extracts default page size when invalid option provided', () => {
+ expect(extractListPaginationParams({ page_size: '25' }).page_size).toBe(
+ MANAGEMENT_DEFAULT_PAGE_SIZE
+ );
+ });
+
+ it('extracts default page size when not a number provided', () => {
+ expect(extractListPaginationParams({ page_size: 'a' }).page_size).toBe(
+ MANAGEMENT_DEFAULT_PAGE_SIZE
+ );
+ });
+
+ it('extracts only last page size when multiple values provided', () => {
+ expect(extractListPaginationParams({ page_size: ['10', '20'] }).page_size).toBe(20);
+ });
+
+ it('extracts proper page size when single valid value provided', () => {
+ expect(extractListPaginationParams({ page_size: '20' }).page_size).toBe(20);
+ });
+ });
+
+ describe('getTrustedAppsListPath()', () => {
+ it('builds proper path when no parameters provided', () => {
+ expect(getTrustedAppsListPath()).toEqual('/trusted_apps');
+ });
+
+ it('builds proper path when empty parameters provided', () => {
+ expect(getTrustedAppsListPath({})).toEqual('/trusted_apps');
+ });
+
+ it('builds proper path when no page index provided', () => {
+ expect(getTrustedAppsListPath({ page_size: 20 })).toEqual('/trusted_apps?page_size=20');
+ });
+
+ it('builds proper path when no page size provided', () => {
+ expect(getTrustedAppsListPath({ page_index: 2 })).toEqual('/trusted_apps?page_index=2');
+ });
+
+ it('builds proper path when both page index and size provided', () => {
+ expect(getTrustedAppsListPath({ page_index: 2, page_size: 20 })).toEqual(
+ '/trusted_apps?page_index=2&page_size=20'
+ );
+ });
+
+ it('builds proper path when page index is equal to default', () => {
+ const path = getTrustedAppsListPath({
+ page_index: MANAGEMENT_DEFAULT_PAGE,
+ page_size: 20,
+ });
+
+ expect(path).toEqual('/trusted_apps?page_size=20');
+ });
+
+ it('builds proper path when page size is equal to default', () => {
+ const path = getTrustedAppsListPath({
+ page_index: 2,
+ page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
+ });
+
+ expect(path).toEqual('/trusted_apps?page_index=2');
+ });
+
+ it('builds proper path when both page index and size are equal to default', () => {
+ const path = getTrustedAppsListPath({
+ page_index: MANAGEMENT_DEFAULT_PAGE,
+ page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
+ });
+
+ expect(path).toEqual('/trusted_apps');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts
index c5ced6f3bcf55..62f360df90192 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.ts
@@ -10,6 +10,9 @@ import { generatePath } from 'react-router-dom';
import querystring from 'querystring';
import {
+ MANAGEMENT_DEFAULT_PAGE,
+ MANAGEMENT_DEFAULT_PAGE_SIZE,
+ MANAGEMENT_PAGE_SIZE_OPTIONS,
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
@@ -86,8 +89,61 @@ export const getPolicyDetailPath = (policyId: string, search?: string) => {
})}${appendSearch(search)}`;
};
-export const getTrustedAppsListPath = (search?: string) => {
- return `${generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, {
+interface ListPaginationParams {
+ page_index: number;
+ page_size: number;
+}
+
+const isDefaultOrMissing = (value: number | undefined, defaultValue: number) => {
+ return value === undefined || value === defaultValue;
+};
+
+const normalizeListPaginationParams = (
+ params?: Partial
+): Partial => {
+ if (params) {
+ return {
+ ...(!isDefaultOrMissing(params.page_index, MANAGEMENT_DEFAULT_PAGE)
+ ? { page_index: params.page_index }
+ : {}),
+ ...(!isDefaultOrMissing(params.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE)
+ ? { page_size: params.page_size }
+ : {}),
+ };
+ } else {
+ return {};
+ }
+};
+
+const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => {
+ const value = query[key];
+
+ return Array.isArray(value) ? value[value.length - 1] : value;
+};
+
+const extractPageIndex = (query: querystring.ParsedUrlQuery): number => {
+ const pageIndex = Number(extractFirstParamValue(query, 'page_index'));
+
+ return !Number.isFinite(pageIndex) || pageIndex < 0 ? MANAGEMENT_DEFAULT_PAGE : pageIndex;
+};
+
+const extractPageSize = (query: querystring.ParsedUrlQuery): number => {
+ const pageSize = Number(extractFirstParamValue(query, 'page_size'));
+
+ return MANAGEMENT_PAGE_SIZE_OPTIONS.includes(pageSize) ? pageSize : MANAGEMENT_DEFAULT_PAGE_SIZE;
+};
+
+export const extractListPaginationParams = (
+ query: querystring.ParsedUrlQuery
+): ListPaginationParams => ({
+ page_index: extractPageIndex(query),
+ page_size: extractPageSize(query),
+});
+
+export const getTrustedAppsListPath = (params?: Partial): string => {
+ const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, {
tabName: AdministrationSubTab.trustedApps,
- })}${appendSearch(search)}`;
+ });
+
+ return `${path}${appendSearch(querystring.stringify(normalizeListPaginationParams(params)))}`;
};
diff --git a/x-pack/plugins/security_solution/public/management/index.ts b/x-pack/plugins/security_solution/public/management/index.ts
index 902ed085bd369..4bd9ac495ada9 100644
--- a/x-pack/plugins/security_solution/public/management/index.ts
+++ b/x-pack/plugins/security_solution/public/management/index.ts
@@ -47,9 +47,7 @@ export class Management {
* Cast the ImmutableReducer to a regular reducer for compatibility with
* the subplugin architecture (which expects plain redux reducers.)
*/
- reducer: {
- management: managementReducer,
- } as ManagementPluginReducer,
+ reducer: { management: managementReducer } as ManagementPluginReducer,
middleware: managementMiddlewareFactory(core, plugins),
},
};
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 68ba71b7bbc94..e8abe37cf0a88 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
@@ -15,9 +15,12 @@ import {
HostPolicyResponseActionStatus,
} from '../../../../../common/endpoint/types';
import { EndpointState, EndpointIndexUIQueryParams } from '../types';
-import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants';
-
-const PAGE_SIZES = Object.freeze([10, 20, 50]);
+import { extractListPaginationParams } from '../../../common/routing';
+import {
+ MANAGEMENT_DEFAULT_PAGE,
+ MANAGEMENT_DEFAULT_PAGE_SIZE,
+ MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+} from '../../../common/constants';
export const listData = (state: Immutable) => state.hosts;
@@ -129,17 +132,17 @@ export const uiQueryParams: (
) => Immutable = createSelector(
(state: Immutable) => state.location,
(location: Immutable['location']) => {
- const data: EndpointIndexUIQueryParams = { page_index: '0', page_size: '10' };
+ const data: EndpointIndexUIQueryParams = {
+ page_index: String(MANAGEMENT_DEFAULT_PAGE),
+ page_size: String(MANAGEMENT_DEFAULT_PAGE_SIZE),
+ };
+
if (location) {
// Removes the `?` from the beginning of query string if it exists
const query = querystring.parse(location.search.slice(1));
+ const paginationParams = extractListPaginationParams(query);
- const keys: Array = [
- 'selected_endpoint',
- 'page_size',
- 'page_index',
- 'show',
- ];
+ const keys: Array = ['selected_endpoint', 'show'];
for (const key of keys) {
const value: string | undefined =
@@ -160,17 +163,10 @@ export const uiQueryParams: (
}
}
- // Check if page size is an expected size, otherwise default to 10
- if (!PAGE_SIZES.includes(Number(data.page_size))) {
- data.page_size = '10';
- }
-
- // Check if page index is a valid positive integer, otherwise default to 0
- const pageIndexAsNumber = Number(data.page_index);
- if (!Number.isFinite(pageIndexAsNumber) || pageIndexAsNumber < 0) {
- data.page_index = '0';
- }
+ data.page_size = String(paginationParams.page_size);
+ data.page_index = String(paginationParams.page_index);
}
+
return data;
}
);
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 8d08ac4e59a87..a569c4f02604b 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
@@ -33,7 +33,7 @@ import {
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { CreateStructuredSelector } from '../../../../common/store';
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
-import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
+import { DEFAULT_POLL_INTERVAL, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants';
import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
@@ -99,7 +99,7 @@ export const EndpointList = () => {
pageIndex,
pageSize,
totalItemCount,
- pageSizeOptions: [10, 20, 50],
+ pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
hidePerPageOptions: false,
};
}, [pageIndex, pageSize, totalItemCount]);
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts
new file mode 100644
index 0000000000000..9308c137cfb9c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { HttpStart } from 'kibana/public';
+import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants';
+import {
+ GetTrustedListAppsResponse,
+ GetTrustedAppsListRequest,
+} from '../../../../../common/endpoint/types/trusted_apps';
+
+export interface TrustedAppsService {
+ getTrustedAppsList(request: GetTrustedAppsListRequest): Promise;
+}
+
+export class TrustedAppsHttpService implements TrustedAppsService {
+ constructor(private http: HttpStart) {}
+
+ async getTrustedAppsList(request: GetTrustedAppsListRequest) {
+ return this.http.get(TRUSTED_APPS_LIST_API, {
+ query: request,
+ });
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts
new file mode 100644
index 0000000000000..5e00d833981ed
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts
@@ -0,0 +1,242 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ UninitialisedResourceState,
+ LoadingResourceState,
+ LoadedResourceState,
+ FailedResourceState,
+ isUninitialisedResourceState,
+ isLoadingResourceState,
+ isLoadedResourceState,
+ isFailedResourceState,
+ getLastLoadedResourceState,
+ getCurrentResourceError,
+ isOutdatedResourceState,
+} from './async_resource_state';
+
+interface TestData {
+ property: string;
+}
+
+const data: TestData = { property: 'value' };
+
+const uninitialisedResourceState: UninitialisedResourceState = {
+ type: 'UninitialisedResourceState',
+};
+
+const loadedResourceState: LoadedResourceState = {
+ type: 'LoadedResourceState',
+ data,
+};
+
+const failedResourceStateInitially: FailedResourceState = {
+ type: 'FailedResourceState',
+ error: {},
+};
+
+const failedResourceStateSubsequently: FailedResourceState = {
+ type: 'FailedResourceState',
+ error: {},
+ lastLoadedState: loadedResourceState,
+};
+
+const loadingResourceStateInitially: LoadingResourceState = {
+ type: 'LoadingResourceState',
+ previousState: uninitialisedResourceState,
+};
+
+const loadingResourceStateAfterSuccess: LoadingResourceState = {
+ type: 'LoadingResourceState',
+ previousState: loadedResourceState,
+};
+
+const loadingResourceStateAfterInitialFailure: LoadingResourceState = {
+ type: 'LoadingResourceState',
+ previousState: failedResourceStateInitially,
+};
+
+const loadingResourceStateAfterSubsequentFailure: LoadingResourceState = {
+ type: 'LoadingResourceState',
+ previousState: failedResourceStateSubsequently,
+};
+
+describe('AsyncResourceState', () => {
+ describe('guards', () => {
+ describe('isUninitialisedResourceState()', () => {
+ it('returns true for UninitialisedResourceState', () => {
+ expect(isUninitialisedResourceState(uninitialisedResourceState)).toBe(true);
+ });
+
+ it('returns false for LoadingResourceState', () => {
+ expect(isUninitialisedResourceState(loadingResourceStateInitially)).toBe(false);
+ });
+
+ it('returns false for LoadedResourceState', () => {
+ expect(isUninitialisedResourceState(loadedResourceState)).toBe(false);
+ });
+
+ it('returns false for FailedResourceState', () => {
+ expect(isUninitialisedResourceState(failedResourceStateInitially)).toBe(false);
+ });
+ });
+
+ describe('isLoadingResourceState()', () => {
+ it('returns false for UninitialisedResourceState', () => {
+ expect(isLoadingResourceState(uninitialisedResourceState)).toBe(false);
+ });
+
+ it('returns true for LoadingResourceState', () => {
+ expect(isLoadingResourceState(loadingResourceStateInitially)).toBe(true);
+ });
+
+ it('returns false for LoadedResourceState', () => {
+ expect(isLoadingResourceState(loadedResourceState)).toBe(false);
+ });
+
+ it('returns false for FailedResourceState', () => {
+ expect(isLoadingResourceState(failedResourceStateInitially)).toBe(false);
+ });
+ });
+
+ describe('isLoadedResourceState()', () => {
+ it('returns false for UninitialisedResourceState', () => {
+ expect(isLoadedResourceState(uninitialisedResourceState)).toBe(false);
+ });
+
+ it('returns false for LoadingResourceState', () => {
+ expect(isLoadedResourceState(loadingResourceStateInitially)).toBe(false);
+ });
+
+ it('returns true for LoadedResourceState', () => {
+ expect(isLoadedResourceState(loadedResourceState)).toBe(true);
+ });
+
+ it('returns false for FailedResourceState', () => {
+ expect(isLoadedResourceState(failedResourceStateInitially)).toBe(false);
+ });
+ });
+
+ describe('isFailedResourceState()', () => {
+ it('returns false for UninitialisedResourceState', () => {
+ expect(isFailedResourceState(uninitialisedResourceState)).toBe(false);
+ });
+
+ it('returns false for LoadingResourceState', () => {
+ expect(isFailedResourceState(loadingResourceStateInitially)).toBe(false);
+ });
+
+ it('returns false for LoadedResourceState', () => {
+ expect(isFailedResourceState(loadedResourceState)).toBe(false);
+ });
+
+ it('returns true for FailedResourceState', () => {
+ expect(isFailedResourceState(failedResourceStateInitially)).toBe(true);
+ });
+ });
+ });
+
+ describe('functions', () => {
+ describe('getLastLoadedResourceState()', () => {
+ it('returns undefined for UninitialisedResourceState', () => {
+ expect(getLastLoadedResourceState(uninitialisedResourceState)).toBeUndefined();
+ });
+
+ it('returns current state for LoadedResourceState', () => {
+ expect(getLastLoadedResourceState(loadedResourceState)).toBe(loadedResourceState);
+ });
+
+ it('returns undefined for initial FailedResourceState', () => {
+ expect(getLastLoadedResourceState(failedResourceStateInitially)).toBeUndefined();
+ });
+
+ it('returns last loaded state for subsequent FailedResourceState', () => {
+ expect(getLastLoadedResourceState(failedResourceStateSubsequently)).toBe(
+ loadedResourceState
+ );
+ });
+
+ it('returns undefined for initial LoadingResourceState', () => {
+ expect(getLastLoadedResourceState(loadingResourceStateInitially)).toBeUndefined();
+ });
+
+ it('returns previous state for LoadingResourceState after success', () => {
+ expect(getLastLoadedResourceState(loadingResourceStateAfterSuccess)).toBe(
+ loadedResourceState
+ );
+ });
+
+ it('returns undefined for LoadingResourceState after initial failure', () => {
+ expect(getLastLoadedResourceState(loadingResourceStateAfterInitialFailure)).toBeUndefined();
+ });
+
+ it('returns previous state for LoadingResourceState after subsequent failure', () => {
+ expect(getLastLoadedResourceState(loadingResourceStateAfterSubsequentFailure)).toBe(
+ loadedResourceState
+ );
+ });
+ });
+
+ describe('getCurrentResourceError()', () => {
+ it('returns undefined for UninitialisedResourceState', () => {
+ expect(getCurrentResourceError(uninitialisedResourceState)).toBeUndefined();
+ });
+
+ it('returns undefined for LoadedResourceState', () => {
+ expect(getCurrentResourceError(loadedResourceState)).toBeUndefined();
+ });
+
+ it('returns error for FailedResourceState', () => {
+ expect(getCurrentResourceError(failedResourceStateSubsequently)).toStrictEqual({});
+ });
+
+ it('returns undefined for LoadingResourceState', () => {
+ expect(getCurrentResourceError(loadingResourceStateAfterSubsequentFailure)).toBeUndefined();
+ });
+ });
+
+ describe('isOutdatedResourceState()', () => {
+ const trueFreshnessTest = (testData: TestData) => true;
+ const falseFreshnessTest = (testData: TestData) => false;
+
+ it('returns true for UninitialisedResourceState', () => {
+ expect(isOutdatedResourceState(uninitialisedResourceState, falseFreshnessTest)).toBe(true);
+ });
+
+ it('returns false for LoadingResourceState', () => {
+ expect(isOutdatedResourceState(loadingResourceStateAfterSuccess, falseFreshnessTest)).toBe(
+ false
+ );
+ });
+
+ it('returns false for LoadedResourceState and fresh data', () => {
+ expect(isOutdatedResourceState(loadedResourceState, trueFreshnessTest)).toBe(false);
+ });
+
+ it('returns true for LoadedResourceState and outdated data', () => {
+ expect(isOutdatedResourceState(loadedResourceState, falseFreshnessTest)).toBe(true);
+ });
+
+ it('returns true for initial FailedResourceState', () => {
+ expect(isOutdatedResourceState(failedResourceStateInitially, falseFreshnessTest)).toBe(
+ true
+ );
+ });
+
+ it('returns false for subsequent FailedResourceState and fresh data', () => {
+ expect(isOutdatedResourceState(failedResourceStateSubsequently, trueFreshnessTest)).toBe(
+ false
+ );
+ });
+
+ it('returns true for subsequent FailedResourceState and outdated data', () => {
+ expect(isOutdatedResourceState(failedResourceStateSubsequently, falseFreshnessTest)).toBe(
+ true
+ );
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts
new file mode 100644
index 0000000000000..4639a50a61865
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/*
+ * this file contains set of types to represent state of asynchronous resource.
+ * Resource is defined as a reference to potential data that is loaded/updated
+ * using asynchronous communication with data source (for example through REST API call).
+ * Asynchronous update implies that next to just having data:
+ * - there is moment in time when data is not loaded/initialised and not in process of loading/updating
+ * - process performing data update can take considerable time which needs to be communicated to user
+ * - update can fail due to multiple reasons and also needs to be communicated to the user
+ */
+
+import { Immutable } from '../../../../../common/endpoint/types';
+import { ServerApiError } from '../../../../common/types';
+
+/**
+ * Data type to represent uninitialised state of asynchronous resource.
+ * This state indicates that no actions to load the data has be taken.
+ */
+export interface UninitialisedResourceState {
+ type: 'UninitialisedResourceState';
+}
+
+/**
+ * Data type to represent loading state of asynchronous resource. Loading state
+ * should be used to indicate that data is in the process of loading/updating.
+ * It contains reference to previous stale state that can be used to present
+ * previous state of resource to the user (like show previous already loaded
+ * data or show previous failure).
+ *
+ * @param Data - type of the data that is referenced by resource state
+ * @param Error - type of the error that can happen during attempt to update data
+ */
+export interface LoadingResourceState {
+ type: 'LoadingResourceState';
+ previousState: StaleResourceState;
+}
+
+/**
+ * Data type to represent loaded state of asynchronous resource. Loaded state
+ * is characterised with reference to the loaded data.
+ *
+ * @param Data - type of the data that is referenced by resource state
+ */
+export interface LoadedResourceState {
+ type: 'LoadedResourceState';
+ data: Data;
+}
+
+/**
+ * Data type to represent failed state of asynchronous resource. Failed state
+ * is characterised with error and can reference last loaded state. Reference
+ * to last loaded state can be used to present previous successfully loaded data.
+ *
+ * @param Data - type of the data that is referenced by resource state
+ * @param Error - type of the error that can happen during attempt to update data
+ */
+export interface FailedResourceState {
+ type: 'FailedResourceState';
+ error: Error;
+ lastLoadedState?: LoadedResourceState;
+}
+
+/**
+ * Data type to represent stale (not loading) state of asynchronous resource.
+ *
+ * @param Data - type of the data that is referenced by resource state
+ * @param Error - type of the error that can happen during attempt to update data
+ */
+export type StaleResourceState =
+ | UninitialisedResourceState
+ | LoadedResourceState
+ | FailedResourceState;
+
+/**
+ * Data type to represent any state of asynchronous resource.
+ *
+ * @param Data - type of the data that is referenced by resource state
+ * @param Error - type of the error that can happen during attempt to update data
+ */
+export type AsyncResourceState =
+ | UninitialisedResourceState
+ | LoadingResourceState
+ | LoadedResourceState
+ | FailedResourceState;
+
+// Set of guards to narrow the type of AsyncResourceState that make further refactoring easier
+
+export const isUninitialisedResourceState = (
+ state: Immutable>
+): state is Immutable => state.type === 'UninitialisedResourceState';
+
+export const isLoadingResourceState = (
+ state: Immutable>
+): state is Immutable> => state.type === 'LoadingResourceState';
+
+export const isLoadedResourceState = (
+ state: Immutable>
+): state is Immutable> => state.type === 'LoadedResourceState';
+
+export const isFailedResourceState = (
+ state: Immutable>
+): state is Immutable> => state.type === 'FailedResourceState';
+
+// Set of functions to work with AsyncResourceState
+
+export const getLastLoadedResourceState = (
+ state: Immutable>
+): Immutable> | undefined => {
+ if (isLoadedResourceState(state)) {
+ return state;
+ } else if (isLoadingResourceState(state)) {
+ return getLastLoadedResourceState(state.previousState);
+ } else if (isFailedResourceState(state)) {
+ return state.lastLoadedState;
+ } else {
+ return undefined;
+ }
+};
+
+export const getCurrentResourceError = (
+ state: Immutable>
+): Immutable | undefined => {
+ return isFailedResourceState(state) ? state.error : undefined;
+};
+
+export const isOutdatedResourceState = (
+ state: AsyncResourceState,
+ isFresh: (data: Data) => boolean
+): boolean =>
+ isUninitialisedResourceState(state) ||
+ (isLoadedResourceState(state) && !isFresh(state.data)) ||
+ (isFailedResourceState(state) &&
+ (!state.lastLoadedState || !isFresh(state.lastLoadedState.data)));
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts
new file mode 100644
index 0000000000000..99bdac57da4be
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './async_resource_state';
+export * from './trusted_apps_list_page_state';
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts
new file mode 100644
index 0000000000000..23f4cfd576c56
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps';
+import { AsyncResourceState } from '.';
+
+export interface PaginationInfo {
+ index: number;
+ size: number;
+}
+
+export interface TrustedAppsListData {
+ items: TrustedApp[];
+ totalItemsCount: number;
+ paginationInfo: PaginationInfo;
+}
+
+export interface TrustedAppsListPageState {
+ listView: {
+ currentListResourceState: AsyncResourceState;
+ currentPaginationInfo: PaginationInfo;
+ };
+ active: boolean;
+}
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts
new file mode 100644
index 0000000000000..2154a0eca462e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AsyncResourceState, TrustedAppsListData } from '../state';
+
+export interface TrustedAppsListResourceStateChanged {
+ type: 'trustedAppsListResourceStateChanged';
+ payload: {
+ newState: AsyncResourceState;
+ };
+}
+
+export type TrustedAppsPageAction = TrustedAppsListResourceStateChanged;
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts
new file mode 100644
index 0000000000000..c5abaae473486
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { applyMiddleware, createStore } from 'redux';
+
+import { createSpyMiddleware } from '../../../../common/store/test_utils';
+
+import {
+ createFailedListViewWithPagination,
+ createLoadedListViewWithPagination,
+ createLoadingListViewWithPagination,
+ createSampleTrustedApps,
+ createServerApiError,
+ createUserChangedUrlAction,
+} from '../test_utils';
+
+import { TrustedAppsService } from '../service';
+import { PaginationInfo, TrustedAppsListPageState } from '../state';
+import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer';
+import { createTrustedAppsPageMiddleware } from './middleware';
+
+const createGetTrustedListAppsResponse = (pagination: PaginationInfo, totalItemsCount: number) => ({
+ data: createSampleTrustedApps(pagination),
+ page: pagination.index,
+ per_page: pagination.size,
+ total: totalItemsCount,
+});
+
+const createTrustedAppsServiceMock = (): jest.Mocked => ({
+ getTrustedAppsList: jest.fn(),
+});
+
+const createStoreSetup = (trustedAppsService: TrustedAppsService) => {
+ const spyMiddleware = createSpyMiddleware();
+
+ return {
+ spyMiddleware,
+ store: createStore(
+ trustedAppsPageReducer,
+ applyMiddleware(
+ createTrustedAppsPageMiddleware(trustedAppsService),
+ spyMiddleware.actionSpyMiddleware
+ )
+ ),
+ };
+};
+
+describe('middleware', () => {
+ describe('refreshing list resource state', () => {
+ it('sets initial state properly', async () => {
+ expect(createStoreSetup(createTrustedAppsServiceMock()).store.getState()).toStrictEqual(
+ initialTrustedAppsPageState
+ );
+ });
+
+ it('refreshes the list when location changes and data gets outdated', async () => {
+ const pagination = { index: 2, size: 50 };
+ const service = createTrustedAppsServiceMock();
+ const { store, spyMiddleware } = createStoreSetup(service);
+
+ service.getTrustedAppsList.mockResolvedValue(
+ createGetTrustedListAppsResponse(pagination, 500)
+ );
+
+ store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
+
+ expect(store.getState()).toStrictEqual({
+ listView: createLoadingListViewWithPagination(pagination),
+ active: true,
+ });
+
+ await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
+
+ expect(store.getState()).toStrictEqual({
+ listView: createLoadedListViewWithPagination(pagination, pagination, 500),
+ active: true,
+ });
+ });
+
+ it('does not refresh the list when location changes and data does not get outdated', async () => {
+ const pagination = { index: 2, size: 50 };
+ const service = createTrustedAppsServiceMock();
+ const { store, spyMiddleware } = createStoreSetup(service);
+
+ service.getTrustedAppsList.mockResolvedValue(
+ createGetTrustedListAppsResponse(pagination, 500)
+ );
+
+ store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
+
+ await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
+
+ store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
+
+ expect(service.getTrustedAppsList).toBeCalledTimes(1);
+ expect(store.getState()).toStrictEqual({
+ listView: createLoadedListViewWithPagination(pagination, pagination, 500),
+ active: true,
+ });
+ });
+
+ it('set list resource state to faile when failing to load data', async () => {
+ const service = createTrustedAppsServiceMock();
+ const { store, spyMiddleware } = createStoreSetup(service);
+
+ service.getTrustedAppsList.mockRejectedValue(createServerApiError('Internal Server Error'));
+
+ store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
+
+ await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
+
+ expect(store.getState()).toStrictEqual({
+ listView: createFailedListViewWithPagination(
+ { index: 2, size: 50 },
+ createServerApiError('Internal Server Error')
+ ),
+ active: true,
+ });
+
+ const infiniteLoopTest = async () => {
+ await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
+ };
+
+ await expect(infiniteLoopTest).rejects.not.toBeNull();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts
new file mode 100644
index 0000000000000..31c301b8dbd2b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Immutable } from '../../../../../common/endpoint/types';
+import { AppAction } from '../../../../common/store/actions';
+import {
+ ImmutableMiddleware,
+ ImmutableMiddlewareAPI,
+ ImmutableMiddlewareFactory,
+} from '../../../../common/store';
+
+import { TrustedAppsHttpService, TrustedAppsService } from '../service';
+
+import {
+ AsyncResourceState,
+ StaleResourceState,
+ TrustedAppsListData,
+ TrustedAppsListPageState,
+} from '../state';
+
+import { TrustedAppsListResourceStateChanged } from './action';
+
+import {
+ getCurrentListResourceState,
+ getLastLoadedListResourceState,
+ getListCurrentPageIndex,
+ getListCurrentPageSize,
+ needsRefreshOfListData,
+} from './selectors';
+
+const createTrustedAppsListResourceStateChangedAction = (
+ newState: Immutable>
+): Immutable => ({
+ type: 'trustedAppsListResourceStateChanged',
+ payload: { newState },
+});
+
+const refreshList = async (
+ store: ImmutableMiddlewareAPI,
+ trustedAppsService: TrustedAppsService
+) => {
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction({
+ type: 'LoadingResourceState',
+ // need to think on how to avoid the casting
+ previousState: getCurrentListResourceState(store.getState()) as Immutable<
+ StaleResourceState
+ >,
+ })
+ );
+
+ try {
+ const pageIndex = getListCurrentPageIndex(store.getState());
+ const pageSize = getListCurrentPageSize(store.getState());
+ const response = await trustedAppsService.getTrustedAppsList({
+ page: pageIndex + 1,
+ per_page: pageSize,
+ });
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction({
+ type: 'LoadedResourceState',
+ data: {
+ items: response.data,
+ totalItemsCount: response.total,
+ paginationInfo: { index: pageIndex, size: pageSize },
+ },
+ })
+ );
+ } catch (error) {
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction({
+ type: 'FailedResourceState',
+ error,
+ lastLoadedState: getLastLoadedListResourceState(store.getState()),
+ })
+ );
+ }
+};
+
+export const createTrustedAppsPageMiddleware = (
+ trustedAppsService: TrustedAppsService
+): ImmutableMiddleware => {
+ return (store) => (next) => async (action) => {
+ next(action);
+
+ // TODO: need to think if failed state is a good condition to consider need for refresh
+ if (action.type === 'userChangedUrl' && needsRefreshOfListData(store.getState())) {
+ await refreshList(store, trustedAppsService);
+ }
+ };
+};
+
+export const trustedAppsPageMiddlewareFactory: ImmutableMiddlewareFactory = (
+ coreStart
+) => createTrustedAppsPageMiddleware(new TrustedAppsHttpService(coreStart.http));
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts
new file mode 100644
index 0000000000000..34325e0cf1398
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer';
+import {
+ createListLoadedResourceState,
+ createLoadedListViewWithPagination,
+ createTrustedAppsListResourceStateChangedAction,
+ createUserChangedUrlAction,
+} from '../test_utils';
+
+describe('reducer', () => {
+ describe('UserChangedUrl', () => {
+ it('makes page state active and extracts pagination parameters', () => {
+ const result = trustedAppsPageReducer(
+ initialTrustedAppsPageState,
+ createUserChangedUrlAction('/trusted_apps', '?page_index=5&page_size=50')
+ );
+
+ expect(result).toStrictEqual({
+ listView: {
+ ...initialTrustedAppsPageState.listView,
+ currentPaginationInfo: { index: 5, size: 50 },
+ },
+ active: true,
+ });
+ });
+
+ it('extracts default pagination parameters when none provided', () => {
+ const result = trustedAppsPageReducer(
+ {
+ ...initialTrustedAppsPageState,
+ listView: {
+ ...initialTrustedAppsPageState.listView,
+ currentPaginationInfo: { index: 5, size: 50 },
+ },
+ },
+ createUserChangedUrlAction('/trusted_apps', '?page_index=b&page_size=60')
+ );
+
+ expect(result).toStrictEqual({
+ ...initialTrustedAppsPageState,
+ active: true,
+ });
+ });
+
+ it('extracts default pagination parameters when invalid provided', () => {
+ const result = trustedAppsPageReducer(
+ {
+ ...initialTrustedAppsPageState,
+ listView: {
+ ...initialTrustedAppsPageState.listView,
+ currentPaginationInfo: { index: 5, size: 50 },
+ },
+ },
+ createUserChangedUrlAction('/trusted_apps')
+ );
+
+ expect(result).toStrictEqual({
+ ...initialTrustedAppsPageState,
+ active: true,
+ });
+ });
+
+ it('makes page state inactive and resets list to uninitialised state when navigating away', () => {
+ const result = trustedAppsPageReducer(
+ { listView: createLoadedListViewWithPagination(), active: true },
+ createUserChangedUrlAction('/endpoints')
+ );
+
+ expect(result).toStrictEqual(initialTrustedAppsPageState);
+ });
+ });
+
+ describe('TrustedAppsListResourceStateChanged', () => {
+ it('sets the current list resource state', () => {
+ const listResourceState = createListLoadedResourceState({ index: 3, size: 50 }, 200);
+ const result = trustedAppsPageReducer(
+ initialTrustedAppsPageState,
+ createTrustedAppsListResourceStateChangedAction(listResourceState)
+ );
+
+ expect(result).toStrictEqual({
+ ...initialTrustedAppsPageState,
+ listView: {
+ ...initialTrustedAppsPageState.listView,
+ currentListResourceState: listResourceState,
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts
new file mode 100644
index 0000000000000..4fdc6f90ef40c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// eslint-disable-next-line import/no-nodejs-modules
+import { parse } from 'querystring';
+import { matchPath } from 'react-router-dom';
+import { ImmutableReducer } from '../../../../common/store';
+import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
+import { UserChangedUrl } from '../../../../common/store/routing/action';
+import { AppAction } from '../../../../common/store/actions';
+import { extractListPaginationParams } from '../../../common/routing';
+import {
+ MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
+ MANAGEMENT_DEFAULT_PAGE,
+ MANAGEMENT_DEFAULT_PAGE_SIZE,
+} from '../../../common/constants';
+
+import { TrustedAppsListResourceStateChanged } from './action';
+import { TrustedAppsListPageState } from '../state';
+
+type StateReducer = ImmutableReducer;
+type CaseReducer = (
+ state: Immutable,
+ action: Immutable
+) => Immutable;
+
+const isTrustedAppsPageLocation = (location: Immutable) => {
+ return (
+ matchPath(location.pathname ?? '', {
+ path: MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
+ exact: true,
+ }) !== null
+ );
+};
+
+const trustedAppsListResourceStateChanged: CaseReducer = (
+ state,
+ action
+) => {
+ return {
+ ...state,
+ listView: {
+ ...state.listView,
+ currentListResourceState: action.payload.newState,
+ },
+ };
+};
+
+const userChangedUrl: CaseReducer = (state, action) => {
+ if (isTrustedAppsPageLocation(action.payload)) {
+ const paginationParams = extractListPaginationParams(parse(action.payload.search.slice(1)));
+
+ return {
+ ...state,
+ listView: {
+ ...state.listView,
+ currentPaginationInfo: {
+ index: paginationParams.page_index,
+ size: paginationParams.page_size,
+ },
+ },
+ active: true,
+ };
+ } else {
+ return initialTrustedAppsPageState;
+ }
+};
+
+export const initialTrustedAppsPageState: TrustedAppsListPageState = {
+ listView: {
+ currentListResourceState: { type: 'UninitialisedResourceState' },
+ currentPaginationInfo: {
+ index: MANAGEMENT_DEFAULT_PAGE,
+ size: MANAGEMENT_DEFAULT_PAGE_SIZE,
+ },
+ },
+ active: false,
+};
+
+export const trustedAppsPageReducer: StateReducer = (
+ state = initialTrustedAppsPageState,
+ action
+) => {
+ switch (action.type) {
+ case 'trustedAppsListResourceStateChanged':
+ return trustedAppsListResourceStateChanged(state, action);
+
+ case 'userChangedUrl':
+ return userChangedUrl(state, action);
+ }
+
+ return state;
+};
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts
new file mode 100644
index 0000000000000..a969e2dee4773
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ getCurrentListResourceState,
+ getLastLoadedListResourceState,
+ getListCurrentPageIndex,
+ getListCurrentPageSize,
+ getListErrorMessage,
+ getListItems,
+ getListTotalItemsCount,
+ isListLoading,
+ needsRefreshOfListData,
+} from './selectors';
+
+import {
+ createDefaultListView,
+ createDefaultPaginationInfo,
+ createListComplexLoadingResourceState,
+ createListFailedResourceState,
+ createListLoadedResourceState,
+ createLoadedListViewWithPagination,
+ createSampleTrustedApps,
+ createUninitialisedResourceState,
+} from '../test_utils';
+
+describe('selectors', () => {
+ describe('needsRefreshOfListData()', () => {
+ it('returns false for outdated resource state and inactive state', () => {
+ expect(needsRefreshOfListData({ listView: createDefaultListView(), active: false })).toBe(
+ false
+ );
+ });
+
+ it('returns true for outdated resource state and active state', () => {
+ expect(needsRefreshOfListData({ listView: createDefaultListView(), active: true })).toBe(
+ true
+ );
+ });
+
+ it('returns true when current loaded page index is outdated', () => {
+ const listView = createLoadedListViewWithPagination({ index: 1, size: 20 });
+
+ expect(needsRefreshOfListData({ listView, active: true })).toBe(true);
+ });
+
+ it('returns true when current loaded page size is outdated', () => {
+ const listView = createLoadedListViewWithPagination({ index: 0, size: 50 });
+
+ expect(needsRefreshOfListData({ listView, active: true })).toBe(true);
+ });
+
+ it('returns false when current loaded data is up to date', () => {
+ const listView = createLoadedListViewWithPagination();
+
+ expect(needsRefreshOfListData({ listView, active: true })).toBe(false);
+ });
+ });
+
+ describe('getCurrentListResourceState()', () => {
+ it('returns current list resource state', () => {
+ const listView = createDefaultListView();
+
+ expect(getCurrentListResourceState({ listView, active: false })).toStrictEqual(
+ createUninitialisedResourceState()
+ );
+ });
+ });
+
+ describe('getLastLoadedListResourceState()', () => {
+ it('returns last loaded list resource state', () => {
+ const listView = {
+ currentListResourceState: createListComplexLoadingResourceState(
+ createDefaultPaginationInfo(),
+ 200
+ ),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(getLastLoadedListResourceState({ listView, active: false })).toStrictEqual(
+ createListLoadedResourceState(createDefaultPaginationInfo(), 200)
+ );
+ });
+ });
+
+ describe('getListItems()', () => {
+ it('returns empty list when no valid data loaded', () => {
+ expect(getListItems({ listView: createDefaultListView(), active: false })).toStrictEqual([]);
+ });
+
+ it('returns last loaded list items', () => {
+ const listView = {
+ currentListResourceState: createListComplexLoadingResourceState(
+ createDefaultPaginationInfo(),
+ 200
+ ),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(getListItems({ listView, active: false })).toStrictEqual(
+ createSampleTrustedApps(createDefaultPaginationInfo())
+ );
+ });
+ });
+
+ describe('getListTotalItemsCount()', () => {
+ it('returns 0 when no valid data loaded', () => {
+ expect(getListTotalItemsCount({ listView: createDefaultListView(), active: false })).toBe(0);
+ });
+
+ it('returns last loaded total items count', () => {
+ const listView = {
+ currentListResourceState: createListComplexLoadingResourceState(
+ createDefaultPaginationInfo(),
+ 200
+ ),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(getListTotalItemsCount({ listView, active: false })).toBe(200);
+ });
+ });
+
+ describe('getListCurrentPageIndex()', () => {
+ it('returns page index', () => {
+ expect(getListCurrentPageIndex({ listView: createDefaultListView(), active: false })).toBe(0);
+ });
+ });
+
+ describe('getListCurrentPageSize()', () => {
+ it('returns page index', () => {
+ expect(getListCurrentPageSize({ listView: createDefaultListView(), active: false })).toBe(20);
+ });
+ });
+
+ describe('getListErrorMessage()', () => {
+ it('returns undefined when not in failed state', () => {
+ const listView = {
+ currentListResourceState: createListComplexLoadingResourceState(
+ createDefaultPaginationInfo(),
+ 200
+ ),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(getListErrorMessage({ listView, active: false })).toBeUndefined();
+ });
+
+ it('returns message when not in failed state', () => {
+ const listView = {
+ currentListResourceState: createListFailedResourceState('Internal Server Error'),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(getListErrorMessage({ listView, active: false })).toBe('Internal Server Error');
+ });
+ });
+
+ describe('isListLoading()', () => {
+ it('returns false when no loading is happening', () => {
+ expect(isListLoading({ listView: createDefaultListView(), active: false })).toBe(false);
+ });
+
+ it('returns true when loading is in progress', () => {
+ const listView = {
+ currentListResourceState: createListComplexLoadingResourceState(
+ createDefaultPaginationInfo(),
+ 200
+ ),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+ };
+
+ expect(isListLoading({ listView, active: false })).toBe(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts
new file mode 100644
index 0000000000000..6fde779ac1cce
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Immutable, TrustedApp } from '../../../../../common/endpoint/types';
+
+import {
+ AsyncResourceState,
+ getCurrentResourceError,
+ getLastLoadedResourceState,
+ isLoadingResourceState,
+ isOutdatedResourceState,
+ LoadedResourceState,
+ PaginationInfo,
+ TrustedAppsListData,
+ TrustedAppsListPageState,
+} from '../state';
+
+const pageInfosEqual = (pageInfo1: PaginationInfo, pageInfo2: PaginationInfo): boolean =>
+ pageInfo1.index === pageInfo2.index && pageInfo1.size === pageInfo2.size;
+
+export const needsRefreshOfListData = (state: Immutable): boolean => {
+ const currentPageInfo = state.listView.currentPaginationInfo;
+ const currentPage = state.listView.currentListResourceState;
+
+ return (
+ state.active &&
+ isOutdatedResourceState(currentPage, (data) =>
+ pageInfosEqual(currentPageInfo, data.paginationInfo)
+ )
+ );
+};
+
+export const getCurrentListResourceState = (
+ state: Immutable
+): Immutable> | undefined => {
+ return state.listView.currentListResourceState;
+};
+
+export const getLastLoadedListResourceState = (
+ state: Immutable
+): Immutable> | undefined => {
+ return getLastLoadedResourceState(state.listView.currentListResourceState);
+};
+
+export const getListItems = (
+ state: Immutable
+): Immutable => {
+ return getLastLoadedResourceState(state.listView.currentListResourceState)?.data.items || [];
+};
+
+export const getListCurrentPageIndex = (state: Immutable): number => {
+ return state.listView.currentPaginationInfo.index;
+};
+
+export const getListCurrentPageSize = (state: Immutable): number => {
+ return state.listView.currentPaginationInfo.size;
+};
+
+export const getListTotalItemsCount = (state: Immutable): number => {
+ return (
+ getLastLoadedResourceState(state.listView.currentListResourceState)?.data.totalItemsCount || 0
+ );
+};
+
+export const getListErrorMessage = (
+ state: Immutable
+): string | undefined => {
+ return getCurrentResourceError(state.listView.currentListResourceState)?.message;
+};
+
+export const isListLoading = (state: Immutable): boolean => {
+ return isLoadingResourceState(state.listView.currentListResourceState);
+};
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts
new file mode 100644
index 0000000000000..fab059a422a2a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts
@@ -0,0 +1,135 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ServerApiError } from '../../../../common/types';
+import { TrustedApp } from '../../../../../common/endpoint/types';
+import { RoutingAction } from '../../../../common/store/routing';
+
+import {
+ AsyncResourceState,
+ FailedResourceState,
+ LoadedResourceState,
+ LoadingResourceState,
+ PaginationInfo,
+ StaleResourceState,
+ TrustedAppsListData,
+ TrustedAppsListPageState,
+ UninitialisedResourceState,
+} from '../state';
+
+import { TrustedAppsListResourceStateChanged } from '../store/action';
+
+const OS_LIST: Array = ['windows', 'macos', 'linux'];
+
+export const createSampleTrustedApps = (paginationInfo: PaginationInfo): TrustedApp[] => {
+ return [...new Array(paginationInfo.size).keys()].map((i) => ({
+ id: String(paginationInfo.index + i),
+ name: `trusted app ${paginationInfo.index + i}`,
+ description: `Trusted App ${paginationInfo.index + i}`,
+ created_at: '1 minute ago',
+ created_by: 'someone',
+ os: OS_LIST[i % 3],
+ entries: [],
+ }));
+};
+
+export const createTrustedAppsListData = (
+ paginationInfo: PaginationInfo,
+ totalItemsCount: number
+) => ({
+ items: createSampleTrustedApps(paginationInfo),
+ totalItemsCount,
+ paginationInfo,
+});
+
+export const createServerApiError = (message: string) => ({
+ statusCode: 500,
+ error: 'Internal Server Error',
+ message,
+});
+
+export const createUninitialisedResourceState = (): UninitialisedResourceState => ({
+ type: 'UninitialisedResourceState',
+});
+
+export const createListLoadedResourceState = (
+ paginationInfo: PaginationInfo,
+ totalItemsCount: number
+): LoadedResourceState => ({
+ type: 'LoadedResourceState',
+ data: createTrustedAppsListData(paginationInfo, totalItemsCount),
+});
+
+export const createListFailedResourceState = (
+ message: string,
+ lastLoadedState?: LoadedResourceState
+): FailedResourceState => ({
+ type: 'FailedResourceState',
+ error: createServerApiError(message),
+ lastLoadedState,
+});
+
+export const createListLoadingResourceState = (
+ previousState: StaleResourceState = createUninitialisedResourceState()
+): LoadingResourceState => ({
+ type: 'LoadingResourceState',
+ previousState,
+});
+
+export const createListComplexLoadingResourceState = (
+ paginationInfo: PaginationInfo,
+ totalItemsCount: number
+): LoadingResourceState =>
+ createListLoadingResourceState(
+ createListFailedResourceState(
+ 'Internal Server Error',
+ createListLoadedResourceState(paginationInfo, totalItemsCount)
+ )
+ );
+
+export const createDefaultPaginationInfo = () => ({ index: 0, size: 20 });
+
+export const createDefaultListView = () => ({
+ currentListResourceState: createUninitialisedResourceState(),
+ currentPaginationInfo: createDefaultPaginationInfo(),
+});
+
+export const createLoadingListViewWithPagination = (
+ currentPaginationInfo: PaginationInfo,
+ previousState: StaleResourceState = createUninitialisedResourceState()
+): TrustedAppsListPageState['listView'] => ({
+ currentListResourceState: { type: 'LoadingResourceState', previousState },
+ currentPaginationInfo,
+});
+
+export const createLoadedListViewWithPagination = (
+ paginationInfo: PaginationInfo = createDefaultPaginationInfo(),
+ currentPaginationInfo: PaginationInfo = createDefaultPaginationInfo(),
+ totalItemsCount: number = 200
+): TrustedAppsListPageState['listView'] => ({
+ currentListResourceState: createListLoadedResourceState(paginationInfo, totalItemsCount),
+ currentPaginationInfo,
+});
+
+export const createFailedListViewWithPagination = (
+ currentPaginationInfo: PaginationInfo,
+ error: ServerApiError,
+ lastLoadedState?: LoadedResourceState
+): TrustedAppsListPageState['listView'] => ({
+ currentListResourceState: { type: 'FailedResourceState', error, lastLoadedState },
+ currentPaginationInfo,
+});
+
+export const createUserChangedUrlAction = (path: string, search: string = ''): RoutingAction => {
+ return { type: 'userChangedUrl', payload: { pathname: path, search, hash: '' } };
+};
+
+export const createTrustedAppsListResourceStateChangedAction = (
+ newState: AsyncResourceState
+): TrustedAppsListResourceStateChanged => ({
+ type: 'trustedAppsListResourceStateChanged',
+ payload: { newState },
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap
new file mode 100644
index 0000000000000..e0f846f5950f7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap
@@ -0,0 +1,5530 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TrustedAppsList renders correctly initially 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No items found
+
+
+ |
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when failed loading data for the first time 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intenal Server Error
+
+
+ |
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when failed loading data for the second time 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intenal Server Error
+
+
+ |
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when loaded data 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ trusted app 0
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 1
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 2
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 3
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 4
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 5
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 6
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 7
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 8
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 9
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 10
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 11
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 12
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 13
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 14
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 15
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 16
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 17
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 18
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 19
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when loading data for the first time 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No items found
+
+
+ |
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when loading data for the second time 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ trusted app 0
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 1
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 2
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 3
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 4
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 5
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 6
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 7
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 8
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 9
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 10
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 11
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 12
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 13
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 14
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 15
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 16
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 17
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 18
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 19
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustedAppsList renders correctly when new page and page sie set (not loading yet) 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ trusted app 0
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 1
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 2
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 3
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 4
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 5
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 6
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 7
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 8
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 9
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 10
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 11
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 12
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 13
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 14
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 15
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 16
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 17
+
+
+ |
+
+
+
+ Linux
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 18
+
+
+ |
+
+
+
+ Windows
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+ trusted app 19
+
+
+ |
+
+
+
+ Mac OS
+
+ |
+
+
+
+ 1 minute ago
+
+ |
+
+
+
+
+ someone
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
index 6f074f3809036..d6e9aee108cf6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
@@ -17,5 +17,7 @@ exports[`TrustedAppsPage rendering 1`] = `
values={Object {}}
/>
}
-/>
+>
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts
new file mode 100644
index 0000000000000..62610610981e0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useSelector } from 'react-redux';
+
+import { State } from '../../../../common/store';
+
+import {
+ MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE as TRUSTED_APPS_NS,
+ MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS,
+} from '../../../common/constants';
+
+import { TrustedAppsListPageState } from '../state';
+
+export function useTrustedAppsSelector(selector: (state: TrustedAppsListPageState) => R): R {
+ return useSelector((state: State) =>
+ selector(state[GLOBAL_NS][TRUSTED_APPS_NS] as TrustedAppsListPageState)
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx
new file mode 100644
index 0000000000000..0362f5c7a9de6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { combineReducers, createStore } from 'redux';
+import { render } from '@testing-library/react';
+import React from 'react';
+import { Provider } from 'react-redux';
+
+import {
+ MANAGEMENT_STORE_GLOBAL_NAMESPACE,
+ MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
+} from '../../../common/constants';
+import { trustedAppsPageReducer } from '../store/reducer';
+import { TrustedAppsList } from './trusted_apps_list';
+import {
+ createListFailedResourceState,
+ createListLoadedResourceState,
+ createListLoadingResourceState,
+ createTrustedAppsListResourceStateChangedAction,
+ createUserChangedUrlAction,
+} from '../test_utils';
+
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
+ htmlIdGenerator: () => () => 'mockId',
+}));
+
+const createStoreSetup = () => {
+ return createStore(
+ combineReducers({
+ [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({
+ [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
+ }),
+ })
+ );
+};
+
+const renderList = (store: ReturnType) => {
+ const Wrapper: React.FC = ({ children }) => {children};
+
+ return render(, { wrapper: Wrapper });
+};
+
+describe('TrustedAppsList', () => {
+ it('renders correctly initially', () => {
+ expect(renderList(createStoreSetup()).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when loading data for the first time', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState())
+ );
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when failed loading data for the first time', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(
+ createListFailedResourceState('Intenal Server Error')
+ )
+ );
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when loaded data', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(
+ createListLoadedResourceState({ index: 0, size: 20 }, 200)
+ )
+ );
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when new page and page sie set (not loading yet)', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(
+ createListLoadedResourceState({ index: 0, size: 20 }, 200)
+ )
+ );
+ store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when loading data for the second time', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(
+ createListLoadingResourceState(createListLoadedResourceState({ index: 0, size: 20 }, 200))
+ )
+ );
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+
+ it('renders correctly when failed loading data for the second time', () => {
+ const store = createStoreSetup();
+
+ store.dispatch(
+ createTrustedAppsListResourceStateChangedAction(
+ createListFailedResourceState(
+ 'Intenal Server Error',
+ createListLoadedResourceState({ index: 0, size: 20 }, 200)
+ )
+ )
+ );
+
+ expect(renderList(store).container).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx
new file mode 100644
index 0000000000000..a9077dd84913e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useCallback, useMemo } from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { Immutable } from '../../../../../common/endpoint/types';
+import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps';
+import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants';
+import { getTrustedAppsListPath } from '../../../common/routing';
+
+import {
+ getListCurrentPageIndex,
+ getListCurrentPageSize,
+ getListErrorMessage,
+ getListItems,
+ getListTotalItemsCount,
+ isListLoading,
+} from '../store/selectors';
+
+import { useTrustedAppsSelector } from './hooks';
+
+import { FormattedDate } from '../../../../common/components/formatted_date';
+
+const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = {
+ windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', {
+ defaultMessage: 'Windows',
+ }),
+ macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', {
+ defaultMessage: 'Mac OS',
+ }),
+ linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', {
+ defaultMessage: 'Linux',
+ }),
+};
+
+const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: string }> = {
+ name: i18n.translate('xpack.securitySolution.trustedapps.list.columns.name', {
+ defaultMessage: 'Name',
+ }),
+ os: i18n.translate('xpack.securitySolution.trustedapps.list.columns.os', {
+ defaultMessage: 'OS',
+ }),
+ created_at: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdAt', {
+ defaultMessage: 'Date Created',
+ }),
+ created_by: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdBy', {
+ defaultMessage: 'Created By',
+ }),
+};
+
+const getColumnDefinitions = (): Array>> => [
+ {
+ field: 'name',
+ name: COLUMN_TITLES.name,
+ },
+ {
+ field: 'os',
+ name: COLUMN_TITLES.os,
+ render(value: TrustedApp['os'], record: Immutable) {
+ return OS_TITLES[value];
+ },
+ },
+ {
+ field: 'created_at',
+ name: COLUMN_TITLES.created_at,
+ render(value: TrustedApp['created_at'], record: Immutable) {
+ return (
+
+ );
+ },
+ },
+ {
+ field: 'created_by',
+ name: COLUMN_TITLES.created_by,
+ },
+];
+
+export const TrustedAppsList = memo(() => {
+ const pageIndex = useTrustedAppsSelector(getListCurrentPageIndex);
+ const pageSize = useTrustedAppsSelector(getListCurrentPageSize);
+ const totalItemCount = useTrustedAppsSelector(getListTotalItemsCount);
+ const listItems = useTrustedAppsSelector(getListItems);
+ const history = useHistory();
+
+ return (
+ [...listItems], [listItems])}
+ error={useTrustedAppsSelector(getListErrorMessage)}
+ loading={useTrustedAppsSelector(isListLoading)}
+ pagination={useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ totalItemCount,
+ hidePerPageOptions: false,
+ pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
+ }),
+ [pageIndex, pageSize, totalItemCount]
+ )}
+ onChange={useCallback(
+ ({ page }: { page: { index: number; size: number } }) => {
+ history.push(
+ getTrustedAppsListPath({
+ page_index: page.index,
+ page_size: page.size,
+ })
+ );
+ },
+ [history]
+ )}
+ />
+ );
+});
+
+TrustedAppsList.displayName = 'TrustedAppsList';
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
index 7045fa49ffad3..c0d3b9cd699de 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
@@ -3,11 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { AdministrationListPage } from '../../../components/administration_list_page';
+import { TrustedAppsList } from './trusted_apps_list';
-export function TrustedAppsPage() {
+export const TrustedAppsPage = memo(() => {
return (
}
- />
+ >
+
+
);
-}
+});
+
+TrustedAppsPage.displayName = 'TrustedAppsPage';
diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts
index c7a7d2cad0623..77d02262e93b7 100644
--- a/x-pack/plugins/security_solution/public/management/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts
@@ -9,22 +9,22 @@ import {
SecuritySubPluginMiddlewareFactory,
State,
} from '../../common/store';
-import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list';
-import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
+ MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
} from '../common/constants';
+import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list';
+import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
+import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware';
+
+type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE];
-const policyListSelector = (state: State) =>
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE];
-const policyDetailsSelector = (state: State) =>
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE];
-const endpointsSelector = (state: State) =>
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE];
+const createSubStateSelector = (namespace: K) => (state: State) =>
+ state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][namespace];
export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
coreStart,
@@ -32,13 +32,20 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
) => {
return [
substateMiddlewareFactory(
- policyListSelector,
+ createSubStateSelector(MANAGEMENT_STORE_POLICY_LIST_NAMESPACE),
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
- policyDetailsSelector,
+ createSubStateSelector(MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE),
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
- substateMiddlewareFactory(endpointsSelector, endpointMiddlewareFactory(coreStart, depsStart)),
+ substateMiddlewareFactory(
+ createSubStateSelector(MANAGEMENT_STORE_ENDPOINTS_NAMESPACE),
+ endpointMiddlewareFactory(coreStart, depsStart)
+ ),
+ substateMiddlewareFactory(
+ createSubStateSelector(MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE),
+ trustedAppsPageMiddlewareFactory(coreStart, depsStart)
+ ),
];
};
diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts
index eafd69c875ff1..29eb2d289ae1c 100644
--- a/x-pack/plugins/security_solution/public/management/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts
@@ -17,6 +17,7 @@ import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
+ MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
} from '../common/constants';
import { ImmutableCombineReducers } from '../../common/store';
import { Immutable } from '../../../common/endpoint/types';
@@ -25,6 +26,10 @@ import {
endpointListReducer,
initialEndpointListState,
} from '../pages/endpoint_hosts/store/reducer';
+import {
+ initialTrustedAppsPageState,
+ trustedAppsPageReducer,
+} from '../pages/trusted_apps/store/reducer';
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
@@ -35,6 +40,7 @@ export const mockManagementState: Immutable = {
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(),
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState,
+ [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState,
};
/**
@@ -44,4 +50,5 @@ export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer,
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer,
+ [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
});
diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts
index 21214241d1981..8b53f4c1d8525 100644
--- a/x-pack/plugins/security_solution/public/management/types.ts
+++ b/x-pack/plugins/security_solution/public/management/types.ts
@@ -8,6 +8,7 @@ import { CombinedState } from 'redux';
import { SecurityPageName } from '../app/types';
import { PolicyListState, PolicyDetailsState } from './pages/policy/types';
import { EndpointState } from './pages/endpoint_hosts/types';
+import { TrustedAppsListPageState } from './pages/trusted_apps/state/trusted_apps_list_page_state';
/**
* The type for the management store global namespace. Used mostly internally to reference
@@ -19,6 +20,7 @@ export type ManagementState = CombinedState<{
policyList: PolicyListState;
policyDetails: PolicyDetailsState;
endpoints: EndpointState;
+ trustedApps: TrustedAppsListPageState;
}>;
/**
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/details/__snapshots__/index.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap
rename to x-pack/plugins/security_solution/public/network/components/details/__snapshots__/index.test.tsx.snap
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx
rename to x-pack/plugins/security_solution/public/network/components/details/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx
similarity index 88%
rename from x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx
rename to x-pack/plugins/security_solution/public/network/components/details/index.tsx
index d6dfe1769308e..2c6840895683c 100644
--- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx
@@ -12,7 +12,7 @@ import React from 'react';
import { DEFAULT_DARK_MODE } from '../../../../common/constants';
import { DescriptionList } from '../../../../common/utility_types';
import { useUiSetting$ } from '../../../common/lib/kibana';
-import { FlowTarget, IpOverviewData, Overview } from '../../../graphql/types';
+import { FlowTarget, NetworkDetailsStrategyResponse } from '../../../../common/search_strategy';
import { networkModel } from '../../store';
import { getEmptyTagValue } from '../../../common/components/empty_value';
@@ -34,8 +34,8 @@ import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_ca
import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions';
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
-interface OwnProps {
- data: IpOverviewData;
+export interface IpOverviewProps {
+ data: NetworkDetailsStrategyResponse['networkDetails'];
flowTarget: FlowTarget;
id: string;
ip: string;
@@ -48,15 +48,11 @@ interface OwnProps {
narrowDateRange: NarrowDateRange;
}
-export type IpOverviewProps = OwnProps;
-
-const getDescriptionList = (descriptionList: DescriptionList[], key: number) => {
- return (
-
-
-
- );
-};
+const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
+
+
+
+);
export const IpOverview = React.memo(
({
@@ -74,7 +70,7 @@ export const IpOverview = React.memo(
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE);
- const typeData: Overview = data[flowTarget]!;
+ const typeData = data[flowTarget]!;
const column: DescriptionList[] = [
{
title: i18n.LOCATION,
@@ -124,13 +120,14 @@ export const IpOverview = React.memo(
[
{
title: i18n.HOST_ID,
- description: typeData
- ? hostIdRenderer({ host: data.host, ipFilter: ip })
- : getEmptyTagValue(),
+ description:
+ typeData && data.host
+ ? hostIdRenderer({ host: data.host, ipFilter: ip })
+ : getEmptyTagValue(),
},
{
title: i18n.HOST_NAME,
- description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(),
+ description: typeData && data.host ? hostNameRenderer(data.host, ip) : getEmptyTagValue(),
},
],
[
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts b/x-pack/plugins/security_solution/public/network/components/details/mock.ts
similarity index 85%
rename from x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts
rename to x-pack/plugins/security_solution/public/network/components/details/mock.ts
index aa86fb177b02a..b1187a9b22d94 100644
--- a/x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts
+++ b/x-pack/plugins/security_solution/public/network/components/details/mock.ts
@@ -4,9 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IpOverviewData } from '../../../graphql/types';
+import { NetworkDetailsStrategyResponse } from '../../../../common/search_strategy';
-export const mockData: Readonly> = {
+export const mockData: Readonly> = {
complete: {
source: {
firstSeen: '2019-02-07T17:19:41.636Z',
@@ -16,7 +19,7 @@ export const mockData: Readonly> = {
continent_name: ['North America'],
city_name: ['New York'],
country_iso_code: ['US'],
- country_name: null,
+ country_name: undefined,
location: {
lat: [40.7214],
lon: [-74.0052],
@@ -33,7 +36,7 @@ export const mockData: Readonly> = {
continent_name: ['North America'],
city_name: ['New York'],
country_iso_code: ['US'],
- country_name: null,
+ country_name: undefined,
location: {
lat: [40.7214],
lon: [-74.0052],
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/translations.ts b/x-pack/plugins/security_solution/public/network/components/details/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/network/components/ip_overview/translations.ts
rename to x-pack/plugins/security_solution/public/network/components/details/translations.ts
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx
index 7c05e93e6876e..27fe27adc99c2 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx
@@ -11,7 +11,7 @@ import '../../../../common/mock/match_media';
import { getRenderedFieldValue, PointToolTipContentComponent } from './point_tool_tip_content';
import { TestProviders } from '../../../../common/mock';
import { getEmptyStringTag } from '../../../../common/components/empty_value';
-import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links';
+import { HostDetailsLink, NetworkDetailsLink } from '../../../../common/components/links';
import { FlowTarget } from '../../../../graphql/types';
import {
TooltipProperty,
@@ -50,17 +50,17 @@ describe('PointToolTipContent', () => {
);
});
- test('it returns IPDetailsLink if field is source.ip', () => {
+ test('it returns NetworkDetailsLink if field is source.ip', () => {
const value = '127.0.0.1';
expect(getRenderedFieldValue('source.ip', value)).toStrictEqual(
-
+
);
});
- test('it returns IPDetailsLink if field is destination.ip', () => {
+ test('it returns NetworkDetailsLink if field is destination.ip', () => {
const value = '127.0.0.1';
expect(getRenderedFieldValue('destination.ip', value)).toStrictEqual(
-
+
);
});
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx
index f38f3e054a645..57113a1395778 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx
@@ -11,7 +11,7 @@ import {
getOrEmptyTagFromValue,
} from '../../../../common/components/empty_value';
import { DescriptionListStyled } from '../../../../common/components/page';
-import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links';
+import { HostDetailsLink, NetworkDetailsLink } from '../../../../common/components/links';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { FlowTarget } from '../../../../graphql/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -67,7 +67,7 @@ export const getRenderedFieldValue = (field: string, value: string) => {
return ;
} else if (['source.ip', 'destination.ip'].includes(field)) {
const flowTarget = field.split('.')[0] as FlowTarget;
- return ;
+ return ;
}
return <>{value}>;
};
diff --git a/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx b/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx
index 3ce623cfc97b8..6ddcf9119b215 100644
--- a/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx
@@ -11,7 +11,7 @@ import { useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { FlowDirection, FlowTarget } from '../../../graphql/types';
-import * as i18nIp from '../ip_overview/translations';
+import * as i18nIp from '../details/translations';
import { FlowTargetSelect } from '../flow_controls/flow_target_select';
import { IpOverviewId } from '../../../timelines/components/field_renderers/field_renderers';
@@ -40,7 +40,7 @@ export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget
const history = useHistory();
const location = useLocation();
- const updateIpDetailsFlowTarget = useCallback(
+ const updateNetworkDetailsFlowTarget = useCallback(
(newFlowTarget: FlowTarget) => {
const newPath = getUpdatedFlowTargetPath(location, flowTarget, newFlowTarget);
history.push(newPath);
@@ -56,7 +56,7 @@ export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget
selectedDirection={FlowDirection.uniDirectional}
selectedTarget={flowTarget}
displayTextOverride={[i18nIp.AS_SOURCE, i18nIp.AS_DESTINATION]}
- updateFlowTargetAction={updateIpDetailsFlowTarget}
+ updateFlowTargetAction={updateNetworkDetailsFlowTarget}
/>
);
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap
index 875a490d86be3..7adee9531b1f3 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap
@@ -1,102 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`NetworkHttp Table Component rendering it renders the default NetworkHttp table 1`] = `
-
-`;
+exports[`NetworkHttp Table Component rendering it renders the default NetworkHttp table 1`] = `null`;
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx
index 0c5856241c5a3..42e52e3a86f43 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx
@@ -8,10 +8,14 @@
import React from 'react';
import numeral from '@elastic/numeral';
-import { NetworkHttpEdges, NetworkHttpFields, NetworkHttpItem } from '../../../graphql/types';
+import {
+ NetworkHttpEdges,
+ NetworkHttpFields,
+ NetworkHttpItem,
+} from '../../../../common/search_strategy/security_solution/network';
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
import { getEmptyTagValue } from '../../../common/components/empty_value';
-import { IPDetailsLink } from '../../../common/components/links';
+import { NetworkDetailsLink } from '../../../common/components/links';
import { Columns } from '../../../common/components/paginated_table';
import * as i18n from './translations';
@@ -98,7 +102,7 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
attrName: 'source.ip',
idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`),
rowItem: lastSourceIp,
- render: () => ,
+ render: () => ,
})
: getEmptyTagValue(),
},
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx
index ed25373018207..ccb238bcee274 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx
@@ -5,17 +5,17 @@
*/
import React, { useCallback, useMemo } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { networkActions, networkModel, networkSelectors } from '../../store';
-import { Direction, NetworkHttpEdges, NetworkHttpFields } from '../../../graphql/types';
+import { NetworkHttpEdges, NetworkHttpFields } from '../../../../common/search_strategy';
import { State } from '../../../common/store';
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table';
import { getNetworkHttpColumns } from './columns';
import * as i18n from './translations';
-interface OwnProps {
+interface NetworkHttpTableProps {
data: NetworkHttpEdges[];
fakeTotalCount: number;
id: string;
@@ -27,8 +27,6 @@ interface OwnProps {
type: networkModel.NetworkType;
}
-type NetworkHttpTableProps = OwnProps & PropsFromRedux;
-
const rowItems: ItemsPerRow[] = [
{
text: i18n.ROWS_5,
@@ -41,60 +39,68 @@ const rowItems: ItemsPerRow[] = [
];
const NetworkHttpTableComponent: React.FC = ({
- activePage,
data,
fakeTotalCount,
id,
isInspect,
- limit,
loading,
loadPage,
showMorePagesIndicator,
- sort,
totalCount,
type,
- updateNetworkTable,
}) => {
+ const dispatch = useDispatch();
+ const getNetworkHttpSelector = networkSelectors.httpSelector();
+ const { activePage, limit, sort } = useSelector(
+ (state: State) => getNetworkHttpSelector(state, type),
+ shallowEqual
+ );
const tableType =
type === networkModel.NetworkType.page
? networkModel.NetworkTableType.http
- : networkModel.IpDetailsTableType.http;
+ : networkModel.NetworkDetailsTableType.http;
const updateLimitPagination = useCallback(
(newLimit) =>
- updateNetworkTable({
- networkType: type,
- tableType,
- updates: { limit: newLimit },
- }),
- [type, updateNetworkTable, tableType]
+ dispatch(
+ networkActions.updateNetworkTable({
+ networkType: type,
+ tableType,
+ updates: { limit: newLimit },
+ })
+ ),
+ [dispatch, type, tableType]
);
const updateActivePage = useCallback(
(newPage) =>
- updateNetworkTable({
- networkType: type,
- tableType,
- updates: { activePage: newPage },
- }),
- [type, updateNetworkTable, tableType]
+ dispatch(
+ networkActions.updateNetworkTable({
+ networkType: type,
+ tableType,
+ updates: { activePage: newPage },
+ })
+ ),
+ [dispatch, type, tableType]
);
const onChange = useCallback(
(criteria: Criteria) => {
if (criteria.sort != null && criteria.sort.direction !== sort.direction) {
- updateNetworkTable({
- networkType: type,
- tableType,
- updates: {
- sort: {
- direction: criteria.sort.direction as Direction,
+ dispatch(
+ networkActions.updateNetworkTable({
+ networkType: type,
+ tableType,
+ updates: {
+ sort: {
+ direction: criteria.sort.direction,
+ },
},
- },
- });
+ })
+ );
}
},
- [tableType, sort.direction, type, updateNetworkTable]
+ [sort.direction, dispatch, type, tableType]
);
const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction };
@@ -128,18 +134,4 @@ const NetworkHttpTableComponent: React.FC = ({
NetworkHttpTableComponent.displayName = 'NetworkHttpTableComponent';
-const makeMapStateToProps = () => {
- const getNetworkHttpSelector = networkSelectors.httpSelector();
- const mapStateToProps = (state: State, { type }: OwnProps) => getNetworkHttpSelector(state, type);
- return mapStateToProps;
-};
-
-const mapDispatchToProps = {
- updateNetworkTable: networkActions.updateNetworkTable,
-};
-
-const connector = connect(makeMapStateToProps, mapDispatchToProps);
-
-type PropsFromRedux = ConnectedProps;
-
-export const NetworkHttpTable = connector(React.memo(NetworkHttpTableComponent));
+export const NetworkHttpTable = React.memo(NetworkHttpTableComponent);
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx
index d6661ed48197a..634bf2ba35d76 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx
@@ -37,7 +37,7 @@ export type NetworkTopCountriesColumns = [
Columns
];
-export type NetworkTopCountriesColumnsIpDetails = [
+export type NetworkTopCountriesColumnsNetworkDetails = [
Columns,
Columns,
Columns,
@@ -164,7 +164,7 @@ export const getCountriesColumnsCurated = (
flowTarget: FlowTargetSourceDest,
type: networkModel.NetworkType,
tableId: string
-): NetworkTopCountriesColumns | NetworkTopCountriesColumnsIpDetails => {
+): NetworkTopCountriesColumns | NetworkTopCountriesColumnsNetworkDetails => {
const columns = getNetworkTopCountriesColumns(indexPattern, flowTarget, type, tableId);
// Columns to exclude from host details pages
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx
index 114bca9f59d9c..2495f9e7c11c8 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx
@@ -88,8 +88,8 @@ const NetworkTopCountriesTableComponent: React.FC
}
return flowTargeted === FlowTargetSourceDest.source
- ? networkModel.IpDetailsTableType.topCountriesSource
- : networkModel.IpDetailsTableType.topCountriesDestination;
+ ? networkModel.NetworkDetailsTableType.topCountriesSource
+ : networkModel.NetworkDetailsTableType.topCountriesDestination;
}, [flowTargeted, type]);
const field =
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx
index 16c26d5077c8d..ae390e8e2ed71 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx
@@ -22,7 +22,7 @@ import {
} from '../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
import { getEmptyTagValue } from '../../../common/components/empty_value';
-import { IPDetailsLink } from '../../../common/components/links';
+import { NetworkDetailsLink } from '../../../common/components/links';
import { Columns } from '../../../common/components/paginated_table';
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
import { Provider } from '../../../timelines/components/timeline/data_providers/provider';
@@ -43,7 +43,7 @@ export type NetworkTopNFlowColumns = [
Columns
];
-export type NetworkTopNFlowColumnsIpDetails = [
+export type NetworkTopNFlowColumnsNetworkDetails = [
Columns,
Columns,
Columns,
@@ -86,7 +86,7 @@ export const getNetworkTopNFlowColumns = (
) : (
-
+
)
}
/>
@@ -239,7 +239,7 @@ export const getNFlowColumnsCurated = (
flowTarget: FlowTargetSourceDest,
type: networkModel.NetworkType,
tableId: string
-): NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails => {
+): NetworkTopNFlowColumns | NetworkTopNFlowColumnsNetworkDetails => {
const columns = getNetworkTopNFlowColumns(flowTarget, tableId);
// Columns to exclude from host details pages
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx
index 30b70872432f9..757b178431d90 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx
@@ -82,8 +82,8 @@ const NetworkTopNFlowTableComponent: React.FC = ({
} else {
tableType =
flowTargeted === FlowTargetSourceDest.source
- ? networkModel.IpDetailsTableType.topNFlowSource
- : networkModel.IpDetailsTableType.topNFlowDestination;
+ ? networkModel.NetworkDetailsTableType.topNFlowSource
+ : networkModel.NetworkDetailsTableType.topNFlowDestination;
}
const onChange = useCallback(
diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap
index 8b7d8efa7ac37..a9c3a1a006268 100644
--- a/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap
@@ -1,79 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Tls Table Component Rendering it renders the default Domains table 1`] = `
-
-`;
+exports[`Tls Table Component Rendering it renders the default Domains table 1`] = `null`;
diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx
index d0e001466518d..3d19eedc06a8e 100644
--- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx
@@ -5,11 +5,16 @@
*/
import React, { useCallback, useMemo } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { networkActions, networkModel, networkSelectors } from '../../store';
-import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../graphql/types';
+import {
+ Direction,
+ NetworkTlsEdges,
+ NetworkTlsFields,
+ SortField,
+} from '../../../../common/search_strategy';
import { State } from '../../../common/store';
import {
Criteria,
@@ -20,8 +25,8 @@ import {
import { getTlsColumns } from './columns';
import * as i18n from './translations';
-interface OwnProps {
- data: TlsEdges[];
+interface TlsTableProps {
+ data: NetworkTlsEdges[];
fakeTotalCount: number;
id: string;
isInspect: boolean;
@@ -32,8 +37,6 @@ interface OwnProps {
type: networkModel.NetworkType;
}
-type TlsTableProps = OwnProps & PropsFromRedux;
-
const rowItems: ItemsPerRow[] = [
{
text: i18n.ROWS_5,
@@ -47,123 +50,115 @@ const rowItems: ItemsPerRow[] = [
export const tlsTableId = 'tls-table';
-const TlsTableComponent = React.memo(
- ({
- activePage,
- data,
- fakeTotalCount,
- id,
- isInspect,
- limit,
- loading,
- loadPage,
- showMorePagesIndicator,
- sort,
- totalCount,
- type,
- updateNetworkTable,
- }) => {
- const tableType: networkModel.TopTlsTableType =
- type === networkModel.NetworkType.page
- ? networkModel.NetworkTableType.tls
- : networkModel.IpDetailsTableType.tls;
-
- const updateLimitPagination = useCallback(
- (newLimit) =>
- updateNetworkTable({
+const TlsTableComponent: React.FC = ({
+ data,
+ fakeTotalCount,
+ id,
+ isInspect,
+ loading,
+ loadPage,
+ showMorePagesIndicator,
+ totalCount,
+ type,
+}) => {
+ const dispatch = useDispatch();
+ const getTlsSelector = networkSelectors.tlsSelector();
+ const { activePage, limit, sort } = useSelector(
+ (state: State) => getTlsSelector(state, type),
+ shallowEqual
+ );
+ const tableType: networkModel.TopTlsTableType =
+ type === networkModel.NetworkType.page
+ ? networkModel.NetworkTableType.tls
+ : networkModel.NetworkDetailsTableType.tls;
+
+ const updateLimitPagination = useCallback(
+ (newLimit) =>
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { limit: newLimit },
- }),
- [type, updateNetworkTable, tableType]
- );
-
- const updateActivePage = useCallback(
- (newPage) =>
- updateNetworkTable({
+ })
+ ),
+ [dispatch, type, tableType]
+ );
+
+ const updateActivePage = useCallback(
+ (newPage) =>
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { activePage: newPage },
- }),
- [type, updateNetworkTable, tableType]
- );
-
- const onChange = useCallback(
- (criteria: Criteria) => {
- if (criteria.sort != null) {
- const splitField = criteria.sort.field.split('.');
- const newTlsSort: TlsSortField = {
- field: getSortFromString(splitField[splitField.length - 1]),
- direction: criteria.sort.direction as Direction,
- };
- if (!deepEqual(newTlsSort, sort)) {
- updateNetworkTable({
+ })
+ ),
+ [dispatch, type, tableType]
+ );
+
+ const onChange = useCallback(
+ (criteria: Criteria) => {
+ if (criteria.sort != null) {
+ const splitField = criteria.sort.field.split('.');
+ const newTlsSort: SortField = {
+ field: getSortFromString(splitField[splitField.length - 1]),
+ direction: criteria.sort.direction as Direction,
+ };
+ if (!deepEqual(newTlsSort, sort)) {
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { sort: newTlsSort },
- });
- }
+ })
+ );
}
- },
- [sort, type, tableType, updateNetworkTable]
- );
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]);
-
- return (
-
- );
- }
-);
-
-TlsTableComponent.displayName = 'TlsTableComponent';
-
-const makeMapStateToProps = () => {
- const getTlsSelector = networkSelectors.tlsSelector();
- return (state: State, { type }: OwnProps) => getTlsSelector(state, type);
+ }
+ },
+ [sort, dispatch, type, tableType]
+ );
+
+ const columns = useMemo(() => getTlsColumns(tlsTableId), []);
+
+ return (
+
+ );
};
-const mapDispatchToProps = {
- updateNetworkTable: networkActions.updateNetworkTable,
-};
-
-const connector = connect(makeMapStateToProps, mapDispatchToProps);
-
-type PropsFromRedux = ConnectedProps;
+TlsTableComponent.displayName = 'TlsTableComponent';
-export const TlsTable = connector(TlsTableComponent);
+export const TlsTable = React.memo(TlsTableComponent);
-const getSortField = (sortField: TlsSortField): SortingBasicTable => ({
+const getSortField = (sortField: SortField): SortingBasicTable => ({
field: `node.${sortField.field}`,
direction: sortField.direction,
});
-const getSortFromString = (sortField: string): TlsFields => {
+const getSortFromString = (sortField: string): NetworkTlsFields => {
switch (sortField) {
case '_id':
- return TlsFields._id;
+ return NetworkTlsFields._id;
default:
- return TlsFields._id;
+ return NetworkTlsFields._id;
}
};
diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap
index 634253d03291f..934a28fbc4dd0 100644
--- a/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap
@@ -1,83 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Users Table Component Rendering it renders the default Users table 1`] = `
-
-`;
+exports[`Users Table Component Rendering it renders the default Users table 1`] = `null`;
diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx
index 9a971e0087d12..0355d0a30cfa4 100644
--- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx
@@ -5,7 +5,7 @@
*/
import React, { useCallback, useMemo } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { assertUnreachable } from '../../../../common/utility_types';
@@ -13,11 +13,10 @@ import { networkActions, networkModel, networkSelectors } from '../../store';
import {
Direction,
FlowTarget,
- UsersEdges,
- UsersFields,
- UsersSortField,
-} from '../../../graphql/types';
-import { State } from '../../../common/store';
+ NetworkUsersEdges,
+ NetworkUsersFields,
+ SortField,
+} from '../../../../common/search_strategy';
import {
Criteria,
ItemsPerRow,
@@ -27,10 +26,10 @@ import {
import { getUsersColumns } from './columns';
import * as i18n from './translations';
-const tableType = networkModel.IpDetailsTableType.users;
+const tableType = networkModel.NetworkDetailsTableType.users;
-interface OwnProps {
- data: UsersEdges[];
+interface UsersTableProps {
+ data: NetworkUsersEdges[];
flowTarget: FlowTarget;
fakeTotalCount: number;
id: string;
@@ -42,8 +41,6 @@ interface OwnProps {
type: networkModel.NetworkType;
}
-type UsersTableProps = OwnProps & PropsFromRedux;
-
const rowItems: ItemsPerRow[] = [
{
text: i18n.ROWS_5,
@@ -57,122 +54,110 @@ const rowItems: ItemsPerRow[] = [
export const usersTableId = 'users-table';
-const UsersTableComponent = React.memo(
- ({
- activePage,
- data,
- fakeTotalCount,
- flowTarget,
- id,
- isInspect,
- limit,
- loading,
- loadPage,
- showMorePagesIndicator,
- totalCount,
- type,
- updateNetworkTable,
- sort,
- }) => {
- const updateLimitPagination = useCallback(
- (newLimit) =>
- updateNetworkTable({
+const UsersTableComponent: React.FC = ({
+ data,
+ fakeTotalCount,
+ flowTarget,
+ id,
+ isInspect,
+ loading,
+ loadPage,
+ showMorePagesIndicator,
+ totalCount,
+ type,
+}) => {
+ const dispatch = useDispatch();
+ const getUsersSelector = networkSelectors.usersSelector();
+ const { activePage, sort, limit } = useSelector(getUsersSelector, shallowEqual);
+ const updateLimitPagination = useCallback(
+ (newLimit) =>
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { limit: newLimit },
- }),
- [type, updateNetworkTable]
- );
-
- const updateActivePage = useCallback(
- (newPage) =>
- updateNetworkTable({
+ })
+ ),
+ [dispatch, type]
+ );
+
+ const updateActivePage = useCallback(
+ (newPage) =>
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { activePage: newPage },
- }),
- [type, updateNetworkTable]
- );
-
- const onChange = useCallback(
- (criteria: Criteria) => {
- if (criteria.sort != null) {
- const splitField = criteria.sort.field.split('.');
- const newUsersSort: UsersSortField = {
- field: getSortFromString(splitField[splitField.length - 1]),
- direction: criteria.sort.direction as Direction,
- };
- if (!deepEqual(newUsersSort, sort)) {
- updateNetworkTable({
+ })
+ ),
+ [dispatch, type]
+ );
+
+ const onChange = useCallback(
+ (criteria: Criteria) => {
+ if (criteria.sort != null) {
+ const splitField = criteria.sort.field.split('.');
+ const newUsersSort: SortField = {
+ field: getSortFromString(splitField[splitField.length - 1]),
+ direction: criteria.sort.direction as Direction,
+ };
+ if (!deepEqual(newUsersSort, sort)) {
+ dispatch(
+ networkActions.updateNetworkTable({
networkType: type,
tableType,
updates: { sort: newUsersSort },
- });
- }
+ })
+ );
}
- },
- [sort, type, updateNetworkTable]
- );
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [
- flowTarget,
- usersTableId,
- ]);
-
- return (
-
- );
- }
-);
+ }
+ },
+ [dispatch, sort, type]
+ );
-UsersTableComponent.displayName = 'UsersTableComponent';
-
-const makeMapStateToProps = () => {
- const getUsersSelector = networkSelectors.usersSelector();
- return (state: State) => ({
- ...getUsersSelector(state),
- });
-};
-
-const mapDispatchToProps = {
- updateNetworkTable: networkActions.updateNetworkTable,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [
+ flowTarget,
+ usersTableId,
+ ]);
+
+ return (
+
+ );
};
-const connector = connect(makeMapStateToProps, mapDispatchToProps);
-
-type PropsFromRedux = ConnectedProps;
+UsersTableComponent.displayName = 'UsersTableComponent';
-export const UsersTable = connector(UsersTableComponent);
+export const UsersTable = React.memo(UsersTableComponent);
-const getSortField = (sortField: UsersSortField): SortingBasicTable => {
+const getSortField = (sortField: SortField): SortingBasicTable => {
switch (sortField.field) {
- case UsersFields.name:
+ case NetworkUsersFields.name:
return {
field: `node.user.${sortField.field}`,
direction: sortField.direction,
};
- case UsersFields.count:
+ case NetworkUsersFields.count:
return {
field: `node.user.${sortField.field}`,
direction: sortField.direction,
@@ -181,13 +166,13 @@ const getSortField = (sortField: UsersSortField): SortingBasicTable => {
return assertUnreachable(sortField.field);
};
-const getSortFromString = (sortField: string): UsersFields => {
+const getSortFromString = (sortField: string): NetworkUsersFields => {
switch (sortField) {
- case UsersFields.name.valueOf():
- return UsersFields.name;
- case UsersFields.count.valueOf():
- return UsersFields.count;
+ case NetworkUsersFields.name.valueOf():
+ return NetworkUsersFields.name;
+ case NetworkUsersFields.count.valueOf():
+ return NetworkUsersFields.count;
default:
- return UsersFields.name;
+ return NetworkUsersFields.name;
}
};
diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts
rename to x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts
diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx
new file mode 100644
index 0000000000000..597f85ff082e2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { noop } from 'lodash/fp';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import deepEqual from 'fast-deep-equal';
+
+import { ESTermQuery } from '../../../../common/typed_json';
+import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
+import { inputsModel } from '../../../common/store';
+import { useKibana } from '../../../common/lib/kibana';
+import { createFilter } from '../../../common/containers/helpers';
+import {
+ DocValueFields,
+ NetworkQueries,
+ NetworkDetailsRequestOptions,
+ NetworkDetailsStrategyResponse,
+} from '../../../../common/search_strategy';
+import { AbortError } from '../../../../../../../src/plugins/data/common';
+import * as i18n from './translations';
+import { getInspectResponse } from '../../../helpers';
+import { InspectResponse } from '../../../types';
+
+const ID = 'networkDetailsQuery';
+
+export interface NetworkDetailsArgs {
+ id: string;
+ inspect: InspectResponse;
+ networkDetails: NetworkDetailsStrategyResponse['networkDetails'];
+ refetch: inputsModel.Refetch;
+ isInspected: boolean;
+}
+
+interface UseNetworkDetails {
+ id?: string;
+ docValueFields: DocValueFields[];
+ ip: string;
+ filterQuery?: ESTermQuery | string;
+ skip: boolean;
+}
+
+export const useNetworkDetails = ({
+ docValueFields,
+ filterQuery,
+ id = ID,
+ skip,
+ ip,
+}: UseNetworkDetails): [boolean, NetworkDetailsArgs] => {
+ const { data, notifications, uiSettings } = useKibana().services;
+ const refetch = useRef(noop);
+ const abortCtrl = useRef(new AbortController());
+ const defaultIndex = uiSettings.get