diff --git a/.eslintrc.js b/.eslintrc.js
index b3a1274d1cbeba..81a883f3397e80 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -185,31 +185,40 @@ module.exports = {
zones: [
{
target: [
- 'src/legacy/**/*',
- 'x-pack/**/*',
- '!x-pack/**/*.test.*',
- '!x-pack/test/**/*',
+ '(src|x-pack)/legacy/**/*',
'(src|x-pack)/plugins/**/(public|server)/**/*',
- 'src/core/(public|server)/**/*',
'examples/**/*',
],
from: [
'src/core/public/**/*',
- '!src/core/public/index.ts',
- '!src/core/public/mocks.ts',
- '!src/core/public/*.test.mocks.ts',
+ '!src/core/public/index.ts', // relative import
+ '!src/core/public/mocks{,.ts}',
+ '!src/core/server/types{,.ts}',
'!src/core/public/utils/**/*',
+ '!src/core/public/*.test.mocks{,.ts}',
'src/core/server/**/*',
- '!src/core/server/index.ts',
- '!src/core/server/mocks.ts',
- '!src/core/server/types.ts',
- '!src/core/server/test_utils.ts',
+ '!src/core/server/index.ts', // relative import
+ '!src/core/server/mocks{,.ts}',
+ '!src/core/server/types{,.ts}',
+ '!src/core/server/test_utils',
// for absolute imports until fixed in
// https://github.com/elastic/kibana/issues/36096
- '!src/core/server/types',
- '!src/core/server/*.test.mocks.ts',
-
+ '!src/core/server/*.test.mocks{,.ts}',
+ ],
+ allowSameFolder: true,
+ errorMessage:
+ 'Plugins may only import from top-level public and server modules in core.',
+ },
+ {
+ target: [
+ '(src|x-pack)/legacy/**/*',
+ '(src|x-pack)/plugins/**/(public|server)/**/*',
+ 'examples/**/*',
+ '!(src|x-pack)/**/*.test.*',
+ '!(x-pack/)?test/**/*',
+ ],
+ from: [
'(src|x-pack)/plugins/**/(public|server)/**/*',
'!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}',
],
diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml
index 544dd577313df3..89e0af270c54dd 100644
--- a/.github/paths-labeller.yml
+++ b/.github/paths-labeller.yml
@@ -8,6 +8,9 @@
- "Feature:ExpressionLanguage":
- "src/plugins/expressions/**/*.*"
- "src/plugins/bfetch/**/*.*"
+ - "Team:apm"
+ - "x-pack/plugins/apm/**/*.*"
+ - "x-pack/legacy/plugins/apm/**/*.*"
- "Team:uptime":
- "x-pack/plugins/uptime/**/*.*"
- "x-pack/legacy/plugins/uptime/**/*.*"
diff --git a/.i18nrc.json b/.i18nrc.json
index d4286a7bd50e0f..dc1be7f5a140b3 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -49,7 +49,7 @@
"visTypeMarkdown": "src/plugins/vis_type_markdown",
"visTypeMetric": "src/plugins/vis_type_metric",
"visTypeTable": "src/plugins/vis_type_table",
- "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud",
+ "visTypeTagCloud": "src/plugins/vis_type_tagcloud",
"visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"],
"visTypeVega": "src/legacy/core_plugins/vis_type_vega",
"visTypeVislib": "src/legacy/core_plugins/vis_type_vislib",
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md
deleted file mode 100644
index 2ef8c797f4054b..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md)
-
-## AggConfigOptions.enabled property
-
-Signature:
-
-```typescript
-enabled?: boolean;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md
deleted file mode 100644
index 8939854ab19cac..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md)
-
-## AggConfigOptions.id property
-
-Signature:
-
-```typescript
-id?: string;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md
index b841d9b04d6a75..ff8055b8cf1b19 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md
@@ -2,21 +2,12 @@
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md)
-## AggConfigOptions interface
+## AggConfigOptions type
Signature:
```typescript
-export interface AggConfigOptions
+export declare type AggConfigOptions = Assign;
```
-
-## Properties
-
-| Property | Type | Description |
-| --- | --- | --- |
-| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | boolean
| |
-| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | string
| |
-| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | Record<string, any>
| |
-| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | string
| |
-| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | IAggType
| |
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md
deleted file mode 100644
index 45219a837cc334..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md)
-
-## AggConfigOptions.params property
-
-Signature:
-
-```typescript
-params?: Record;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md
deleted file mode 100644
index b2b42eb2e5b4d9..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md)
-
-## AggConfigOptions.schema property
-
-Signature:
-
-```typescript
-schema?: string;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md
deleted file mode 100644
index 866065ce52ba62..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md)
-
-## AggConfigOptions.type property
-
-Signature:
-
-```typescript
-type: IAggType;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md
index 43f30d73ca6df3..a91db7e7aac8b7 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
+makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md
index b75065da91abda..f9733529a315de 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md
@@ -21,5 +21,5 @@ export declare class AggParamType ex
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | string[]
| |
-| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: any) => TAggConfig
| |
+| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig
| |
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 5ef317a57fb0ea..604ac5120922bf 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
@@ -49,7 +49,6 @@
| Interface | Description |
| --- | --- |
-| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
| [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | |
| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | |
| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | |
@@ -118,6 +117,7 @@
| Type Alias | Description |
| --- | --- |
+| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
| [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | |
diff --git a/docs/images/report-automate-csv.png b/docs/images/report-automate-csv.png
new file mode 100644
index 00000000000000..fba77821ae29f7
Binary files /dev/null and b/docs/images/report-automate-csv.png differ
diff --git a/docs/images/report-automate-pdf.png b/docs/images/report-automate-pdf.png
new file mode 100644
index 00000000000000..f96eebe6fe02d5
Binary files /dev/null and b/docs/images/report-automate-pdf.png differ
diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc
index 3db13acfb423e0..4c408da92f5791 100644
--- a/docs/user/alerting/pre-configured-connectors.asciidoc
+++ b/docs/user/alerting/pre-configured-connectors.asciidoc
@@ -20,8 +20,7 @@ action are predefined, including the connector name and ID.
The following example shows a valid configuration 2 out-of-the box connector.
-[source,console]
-------------------------
+```js
xpack.actions.preconfigured:
- id: 'my-slack1' <1>
actionTypeId: .slack <2>
@@ -40,7 +39,7 @@ The following example shows a valid configuration 2 out-of-the box connector.
secrets: <5>
user: elastic
password: changeme
-------------------------
+```
<1> `id` is the action connector identifier.
<2> `actionTypeId` is the action type identifier.
diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc
index 5d35f103ecee09..3e227229ddcc52 100644
--- a/docs/user/reporting/automating-report-generation.asciidoc
+++ b/docs/user/reporting/automating-report-generation.asciidoc
@@ -1,32 +1,42 @@
[role="xpack"]
[[automating-report-generation]]
== Automating report generation
-You can automatically generate reports with {watcher}, or by submitting
-HTTP `POST` requests from a script.
+Automatically generate PDF and CSV reports by submitting HTTP `POST` requests using {watcher} or a script.
include::report-intervals.asciidoc[]
[float]
-=== Get the POST URL
+=== Create a POST URL
-Generating a report either through {watcher} or a script requires capturing the **POST
-URL**, which is the URL to queue a report for generation.
+Create the POST
+URL that triggers a report to generate.
-To get the URL for triggering PDF report generation during a given time period:
+To create the POST URL for PDF reports:
-. Load the saved object in *Visualize* or *Dashboard*.
-. To specify a relative or absolute time period, use the time filter.
-. In the {kib} toolbar, click *Share*.
-. Select *PDF Reports*.
-. Click **Copy POST URL**.
+. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard.
++
+To specify a relative or absolute time period, use the time filter.
-To get the URL for triggering CSV report generation during a given time period:
+. From the {kib} toolbar, click *Share*, then select *PDF Reports*.
+
+. Click *Copy POST URL*.
++
+[role="screenshot"]
+image::images/report-automate-pdf.png[Generate Visualize and Dashboard reports]
+
+
+To create the POST URL for CSV reports:
. Load the saved search in *Discover*.
-. To specify a relative or absolute time period, use the time filter.
-. In the {kib} toolbar, click *Share*.
-. Select *CSV Reports*.
-. Click **Copy POST URL**.
++
+To specify a relative or absolute time period, use the time filter.
+
+. From the {kib} toolbar, click *Share*, then select *CSV Reports*.
+
+. Click *Copy POST URL*.
++
+[role="screenshot"]
+image::images/report-automate-csv.png[Generate Discover reports]
[float]
=== Use Watcher
diff --git a/docs/visualize/color-picker.asciidoc b/docs/visualize/color-picker.asciidoc
deleted file mode 100644
index e0f23262068d39..00000000000000
--- a/docs/visualize/color-picker.asciidoc
+++ /dev/null
@@ -1,4 +0,0 @@
-You can customize the colors of your visualization by clicking the color dot next to each label to display the
-_color picker_.
-
-image::images/color-picker.png[An array of color dots that users can select]
diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc
index b181763c0d0d00..422afbb201183e 100644
--- a/docs/visualize/lens.asciidoc
+++ b/docs/visualize/lens.asciidoc
@@ -14,20 +14,6 @@ beta[]
* Save your visualization for use in a dashboard.
-[float]
-[[lens-aggregation]]
-=== Supported aggregations
-
-Lens supports the following aggregations:
-
-* <>
-
-* <>
-
-* <>
-
-* <>
-
[float]
[[drag-drop]]
=== Drag and drop
diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc
index ba291e3cc6859a..f716930e7e65c1 100644
--- a/docs/visualize/most-frequent.asciidoc
+++ b/docs/visualize/most-frequent.asciidoc
@@ -13,20 +13,6 @@ The most frequently used visualizations include:
[[metric-chart]]
-[float]
-[[frequently-used-viz-aggregation]]
-=== Supported aggregations
-
-The most frequently used visualizations support the following aggregations:
-
-* <>
-
-* <>
-
-* <>
-
-* <>
-
[float]
=== Configure your visualization
diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc
index 51342847080e0e..c889bd0bb6ca07 100644
--- a/docs/visualize/tilemap.asciidoc
+++ b/docs/visualize/tilemap.asciidoc
@@ -6,19 +6,6 @@ Display graphical representations of data where the individual values are repres
[role="screenshot"]
image::images/visualize_heat_map_example.png[]
-[float]
-[[build-heat-map]]
-=== Build a heat map
-
-To display your data on the heat map, use the supported aggregations.
-
-Heat maps support the following aggregations:
-
-* <>
-* <>
-* <>
-* <>
-
[float]
[[navigate-heatmap]]
=== Change the color ranges
diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc
index 69d6985acd1e42..36709c2cc64370 100644
--- a/docs/visualize/tsvb.asciidoc
+++ b/docs/visualize/tsvb.asciidoc
@@ -43,18 +43,6 @@ Table:: Display data from multiple time series by defining the field group to sh
[role="screenshot"]
image:images/tsvb-table.png["Table visualization"]
-[float]
-[[tsvb-aggregation]]
-=== Supported aggregations
-
-TSVB supports the following aggregations:
-
-* <>
-
-* <>
-
-* <>
-
[float]
[[create-tsvb-visualization]]
=== Create TSVB visualizations
diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc
deleted file mode 100644
index 481cbc6e394182..00000000000000
--- a/docs/visualize/visualize_rollup_data.asciidoc
+++ /dev/null
@@ -1,43 +0,0 @@
-[role="xpack"]
-[[visualize-rollup-data]]
-== Use rolled up data in a visualization
-
-beta[]
-
-You can visualize your rolled up data in a variety of charts, tables, maps, and
-more. Most visualizations support rolled up data, with the exception of
-Timelion and Vega visualizations.
-
-To get started, go to *Management > Kibana > Index patterns.*
-If a rollup index is detected in the cluster, *Create index pattern*
-includes an item for creating a rollup index pattern.
-
-[role="screenshot"]
-image::images/management_create_rollup_menu.png[Create index pattern menu]
-
-You can match an index pattern to only rolled up data, or mix both rolled up
-and raw data to visualize all data together. An index pattern can match only one
-rolled up index, not multiple. There is no restriction on the number of standard
-indices that an index pattern can match. When matching multiple indices,
-use a comma to separate the names, with no space after the comma.
-
-Keep the following in mind when creating a visualization from rolled up data:
-
-* The data in a rollup index only has summarized metrics for specific fields.
-You can’t search any other field from the original raw data.
-* Data is summarized into time buckets that might be split into sub buckets for
-numeric field values or terms. You can ask for a time aggregation that takes
-several time buckets and combines them to lower granularity. For example,
-if the rollup job was aggregated by hours, you can ask for buckets of days.
-
-The following visualization of rolled up data shows the date histogram
-interval multiple and the limited metrics aggregations.
-
-[role="screenshot"]
-image::images/management_rollups_visualization.png[][Rollups in visualizations]
-
-Dashboards can have a mixture of rollup visualizations and regular visualizations,
-as shown in the following figure. Note that not all queries and filters support rollups.
-
-[role="screenshot"]
-image::images/management_rolled_dashboard.png[][Rollups in dashboards]
diff --git a/package.json b/package.json
index a7729b6dab7a16..9bb308d4cdcf11 100644
--- a/package.json
+++ b/package.json
@@ -120,7 +120,7 @@
"@babel/core": "^7.9.0",
"@babel/register": "^7.9.0",
"@elastic/apm-rum": "^5.1.1",
- "@elastic/charts": "18.3.0",
+ "@elastic/charts": "18.4.1",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.8.0",
"@elastic/eui": "22.3.0",
diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json
index b82c8d0fac897b..1a2f6941c20200 100644
--- a/packages/kbn-babel-preset/package.json
+++ b/packages/kbn-babel-preset/package.json
@@ -15,6 +15,7 @@
"babel-plugin-add-module-exports": "^1.0.2",
"babel-plugin-filter-imports": "^3.0.0",
"babel-plugin-styled-components": "^1.10.6",
- "babel-plugin-transform-define": "^1.3.1"
+ "babel-plugin-transform-define": "^1.3.1",
+ "babel-plugin-transform-imports": "^2.0.0"
}
}
diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js
index d76a3e9714838d..2c1129f275bfe3 100644
--- a/packages/kbn-babel-preset/webpack_preset.js
+++ b/packages/kbn-babel-preset/webpack_preset.js
@@ -42,5 +42,24 @@ module.exports = () => {
},
],
],
+ // NOTE: we can enable this by default for everything as soon as we only have one instance
+ // of lodash across the entire project. For now we are just enabling it for siem
+ // as they are extensively using the lodash v4
+ overrides: [
+ {
+ test: [/x-pack[\/\\]legacy[\/\\]plugins[\/\\]siem[\/\\]public/],
+ plugins: [
+ [
+ require.resolve('babel-plugin-transform-imports'),
+ {
+ 'lodash/?(((\\w*)?/?)*)': {
+ transform: 'lodash/${1}/${member}',
+ preventFullImport: false,
+ },
+ },
+ ],
+ ],
+ },
+ ],
};
};
diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
index 4b4bb1282d9395..fe0f75c05c6466 100644
--- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
+++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
@@ -57,6 +57,6 @@ OptimizerConfig {
}
`;
-exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i
`/${bundle.type}:${bundle.id}/${Path.relative(
bundle.sourceRoot,
@@ -146,6 +144,13 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) {
],
rules: [
+ {
+ include: Path.join(bundle.contextDir, bundle.entry),
+ loader: UiSharedDeps.publicPathLoader,
+ options: {
+ key: bundle.id,
+ },
+ },
{
test: /\.css$/,
include: /node_modules/,
@@ -289,6 +294,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) {
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json'],
+ mainFields: ['browser', 'main'],
alias: {
tinymath: require.resolve('tinymath/lib/tinymath.es5.js'),
},
diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts
index dec519da696414..b829c87d91c4ab 100644
--- a/packages/kbn-ui-shared-deps/index.d.ts
+++ b/packages/kbn-ui-shared-deps/index.d.ts
@@ -53,3 +53,8 @@ export const lightCssDistFilename: string;
export const externals: {
[key: string]: string;
};
+
+/**
+ * Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`.
+ */
+export const publicPathLoader: string;
diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js
index 666ec7a46ff06e..42ed08259ac8fd 100644
--- a/packages/kbn-ui-shared-deps/index.js
+++ b/packages/kbn-ui-shared-deps/index.js
@@ -64,3 +64,4 @@ exports.externals = {
'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser',
'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser',
};
+exports.publicPathLoader = require.resolve('./public_path_loader');
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 3aa01179c00f31..46f55da87575db 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 --watch"
},
"dependencies": {
- "@elastic/charts": "18.3.0",
+ "@elastic/charts": "18.4.1",
"@elastic/eui": "22.3.0",
"@kbn/i18n": "1.0.0",
"abortcontroller-polyfill": "^1.4.0",
diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js
new file mode 100644
index 00000000000000..6b7a27c9ca52b6
--- /dev/null
+++ b/packages/kbn-ui-shared-deps/public_path_loader.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = function(source) {
+ const options = this.query;
+ return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`;
+};
diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js
index a8752745449055..bf63c577658595 100644
--- a/packages/kbn-ui-shared-deps/webpack.config.js
+++ b/packages/kbn-ui-shared-deps/webpack.config.js
@@ -46,7 +46,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({
path: UiSharedDeps.distDir,
filename: '[name].js',
sourceMapFilename: '[file].map',
- publicPath: '__REPLACE_WITH_PUBLIC_PATH__',
devtoolModuleFilenameTemplate: info =>
`kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`,
library: '__kbnSharedDeps__',
@@ -55,6 +54,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({
module: {
noParse: [MOMENT_SRC],
rules: [
+ {
+ include: [require.resolve('./entry.js')],
+ use: [
+ {
+ loader: UiSharedDeps.publicPathLoader,
+ options: {
+ key: 'kbn-ui-shared-deps',
+ },
+ },
+ ],
+ },
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts
index d4aced68945267..3cd8ef61690908 100644
--- a/src/core/public/http/index.ts
+++ b/src/core/public/http/index.ts
@@ -18,4 +18,5 @@
*/
export { HttpService } from './http_service';
+export { HttpFetchError } from './http_fetch_error';
export * from './types';
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 254cac3495599b..b4f64125a03efd 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -143,6 +143,7 @@ export {
export {
HttpHeadersInit,
HttpRequestInit,
+ HttpFetchError,
HttpFetchOptions,
HttpFetchOptionsWithPath,
HttpFetchQuery,
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 6d95d1bc7069c5..b92bb209d26073 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -593,6 +593,23 @@ export type HandlerFunction = (context: T, ...args: any[]) =>
// @public
export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never;
+// @internal (undocumented)
+export class HttpFetchError extends Error implements IHttpFetchError {
+ constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any);
+ // (undocumented)
+ readonly body?: any;
+ // (undocumented)
+ readonly name: string;
+ // (undocumented)
+ readonly req: Request;
+ // (undocumented)
+ readonly request: Request;
+ // (undocumented)
+ readonly res?: Response;
+ // (undocumented)
+ readonly response?: Response | undefined;
+}
+
// @public
export interface HttpFetchOptions extends HttpRequestInit {
asResponse?: boolean;
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 3b9a39db722789..2451b98ffdf29d 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -45,6 +45,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.
export { httpServiceMock } from './http/http_service.mock';
export { loggingServiceMock } from './logging/logging_service.mock';
export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock';
+export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
export { metricsServiceMock } from './metrics/metrics_service.mock';
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index 7da14e0dfe51bf..43a2cbd78c5023 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -63,6 +63,7 @@ export default {
'/src/dev/jest/mocks/file_mock.js',
'\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js',
'\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js',
+ '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js',
},
setupFiles: [
'/src/dev/jest/setup/babel_polyfill.js',
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js
similarity index 98%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js
index 152efe5667f183..8f08f6a1f37e63 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js
+++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js
@@ -21,7 +21,6 @@ import expect from '@kbn/expect';
import _ from 'lodash';
import d3 from 'd3';
-import { TagCloud } from '../tag_cloud';
import { fromNode, delay } from 'bluebird';
import { ImageComparator } from 'test_utils/image_comparator';
import simpleloadPng from './simpleload.png';
@@ -29,6 +28,9 @@ import simpleloadPng from './simpleload.png';
// Replace with mock when converting to jest tests
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors';
+// Will be replaced with new path when tests are moved
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud';
describe('tag cloud tests', function() {
const minValue = 1;
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js
similarity index 89%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js
rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js
index 9e611861417cd1..040ee18916fa2d 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js
+++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js
@@ -20,7 +20,6 @@
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { ImageComparator } from 'test_utils/image_comparator';
-import { createTagCloudVisualization } from '../tag_cloud_visualization';
import basicdrawPng from './basicdraw.png';
import afterresizePng from './afterresize.png';
import afterparamChange from './afterparamchange.png';
@@ -32,7 +31,14 @@ import { ExprVis } from '../../../../../../plugins/visualizations/public/express
import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type';
-import { createTagCloudVisTypeDefinition } from '../../tag_cloud_type';
+// Will be replaced with new path when tests are moved
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization';
+import { npStart } from 'ui/new_platform';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services';
const THRESHOLD = 0.65;
const PIXEL_DIFF = 64;
@@ -66,6 +72,8 @@ describe('TagCloudVisualizationTest', function() {
},
});
+ before(() => setFormatService(npStart.plugins.data.fieldFormats));
+
beforeEach(ngMock.module('kibana'));
describe('TagCloudVisualization - basics', function() {
diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js
index 5e78047088d2a3..e1966a9e8b2667 100644
--- a/src/legacy/core_plugins/tests_bundle/index.js
+++ b/src/legacy/core_plugins/tests_bundle/index.js
@@ -148,6 +148,19 @@ export default kibana => {
.type('text/css');
},
});
+
+ // Sets global variables normally set by the bootstrap.js script
+ kbnServer.server.route({
+ path: '/test_bundle/karma/globals.js',
+ method: 'GET',
+ async handler(req, h) {
+ const basePath = config.get('server.basePath');
+
+ const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`;
+
+ return h.response(file).header('content-type', 'application/json');
+ },
+ });
},
__globalImportAliases__: {
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/index.ts
deleted file mode 100644
index 6f768131f21908..00000000000000
--- a/src/legacy/core_plugins/vis_type_tagcloud/index.ts
+++ /dev/null
@@ -1,44 +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 { resolve } from 'path';
-import { Legacy } from 'kibana';
-
-import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types';
-
-const tagCloudPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
- new Plugin({
- id: 'tagcloud',
- require: ['kibana', 'elasticsearch'],
- publicDir: resolve(__dirname, 'public'),
- uiExports: {
- styleSheetPaths: resolve(__dirname, 'public/index.scss'),
- hacks: [resolve(__dirname, 'public/legacy')],
- injectDefaultVars: server => ({}),
- },
- init: (server: Legacy.Server) => ({}),
- config(Joi: any) {
- return Joi.object({
- enabled: Joi.boolean().default(true),
- }).default();
- },
- } as Legacy.PluginSpecOptions);
-
-// eslint-disable-next-line import/no-default-export
-export default tagCloudPluginInitializer;
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/package.json b/src/legacy/core_plugins/vis_type_tagcloud/package.json
deleted file mode 100644
index 4200ef264fece5..00000000000000
--- a/src/legacy/core_plugins/vis_type_tagcloud/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "tagcloud",
- "version": "kibana"
-}
diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
index 7250fa4fc9ecae..8a71c6ccb15064 100644
--- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs
+++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
@@ -1,6 +1,7 @@
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
window.__kbnDarkMode__ = {{darkMode}};
+window.__kbnPublicPath__ = {{publicPathMap}};
if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
var legacyBrowserError = document.getElementById('kbn_legacy_browser_error');
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index 1e84405dd51538..801eecf5b608bd 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -153,11 +153,25 @@ export function uiRenderMixin(kbnServer, server, config) {
`${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`,
];
+ const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()];
+
+ // These paths should align with the bundle routes configured in
+ // src/optimize/bundles_route/bundles_route.js
+ const publicPathMap = JSON.stringify({
+ core: `${regularBundlePath}/core/`,
+ 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`,
+ ...uiPluginIds.reduce(
+ (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }),
+ {}
+ ),
+ });
+
const bootstrap = new AppBootstrap({
templateData: {
darkMode,
jsDependencyPaths,
styleSheetPaths,
+ publicPathMap,
entryBundlePath: isCore
? `${regularBundlePath}/core/core.entry.js`
: `${regularBundlePath}/${app.getId()}.bundle.js`,
diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js
index 530dabb9a46a39..4030988c8552c7 100644
--- a/src/optimize/bundles_route/bundles_route.js
+++ b/src/optimize/bundles_route/bundles_route.js
@@ -72,43 +72,57 @@ export function createBundlesRoute({
}
return [
- buildRouteForBundles(
- `${basePublicPath}/bundles/kbn-ui-shared-deps/`,
- '/bundles/kbn-ui-shared-deps/',
- UiSharedDeps.distDir,
- fileHashCache
- ),
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`,
+ routePath: '/bundles/kbn-ui-shared-deps/',
+ bundlesPath: UiSharedDeps.distDir,
+ fileHashCache,
+ replacePublicPath: false,
+ }),
...npUiPluginPublicDirs.map(({ id, path }) =>
- buildRouteForBundles(
- `${basePublicPath}/bundles/plugin/${id}/`,
- `/bundles/plugin/${id}/`,
- path,
- fileHashCache
- )
- ),
- buildRouteForBundles(
- `${basePublicPath}/bundles/core/`,
- `/bundles/core/`,
- fromRoot(join('src', 'core', 'target', 'public')),
- fileHashCache
- ),
- buildRouteForBundles(
- `${basePublicPath}/bundles/`,
- '/bundles/',
- regularBundlesPath,
- fileHashCache
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/bundles/plugin/${id}/`,
+ routePath: `/bundles/plugin/${id}/`,
+ bundlesPath: path,
+ fileHashCache,
+ replacePublicPath: false,
+ })
),
- buildRouteForBundles(
- `${basePublicPath}/built_assets/dlls/`,
- '/built_assets/dlls/',
- dllBundlesPath,
- fileHashCache
- ),
- buildRouteForBundles(`${basePublicPath}/`, '/built_assets/css/', builtCssPath, fileHashCache),
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/bundles/core/`,
+ routePath: `/bundles/core/`,
+ bundlesPath: fromRoot(join('src', 'core', 'target', 'public')),
+ fileHashCache,
+ replacePublicPath: false,
+ }),
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/bundles/`,
+ routePath: '/bundles/',
+ bundlesPath: regularBundlesPath,
+ fileHashCache,
+ }),
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/built_assets/dlls/`,
+ routePath: '/built_assets/dlls/',
+ bundlesPath: dllBundlesPath,
+ fileHashCache,
+ }),
+ buildRouteForBundles({
+ publicPath: `${basePublicPath}/`,
+ routePath: '/built_assets/css/',
+ bundlesPath: builtCssPath,
+ fileHashCache,
+ }),
];
}
-function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) {
+function buildRouteForBundles({
+ publicPath,
+ routePath,
+ bundlesPath,
+ fileHashCache,
+ replacePublicPath = true,
+}) {
return {
method: 'GET',
path: `${routePath}{path*}`,
@@ -129,6 +143,7 @@ function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache)
bundlesPath,
fileHashCache,
publicPath,
+ replacePublicPath,
});
},
},
diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js
index 7af780a79e4308..80c49a26270fd6 100644
--- a/src/optimize/bundles_route/dynamic_asset_response.js
+++ b/src/optimize/bundles_route/dynamic_asset_response.js
@@ -52,7 +52,7 @@ import { replacePlaceholder } from '../public_path_placeholder';
* @property {LruCache} options.fileHashCache
*/
export async function createDynamicAssetResponse(options) {
- const { request, h, bundlesPath, publicPath, fileHashCache } = options;
+ const { request, h, bundlesPath, publicPath, fileHashCache, replacePublicPath } = options;
let fd;
try {
@@ -78,11 +78,14 @@ export async function createDynamicAssetResponse(options) {
});
fd = null; // read stream is now responsible for fd
+ const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read;
+ const etag = replacePublicPath ? `${hash}-${publicPath}` : hash;
+
return h
- .response(replacePlaceholder(read, publicPath))
+ .response(content)
.takeover()
.code(200)
- .etag(`${hash}-${publicPath}`)
+ .etag(etag)
.header('cache-control', 'must-revalidate')
.type(request.server.mime.path(path).type);
} catch (error) {
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index 49ef1000d7993f..924fcd6730f932 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -139,6 +139,7 @@ export class DataPublicPlugin implements Plugin;
- // (undocumented)
- schema?: string;
- // (undocumented)
+export type AggConfigOptions = Assign;
// Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -112,7 +105,7 @@ export class AggParamType extends Ba
// (undocumented)
allowedAggs: string[];
// (undocumented)
- makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
+ makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
}
// Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts
index 2813e3b9c53733..b5df90313230cf 100644
--- a/src/plugins/data/public/search/aggs/agg_config.test.ts
+++ b/src/plugins/data/public/search/aggs/agg_config.test.ts
@@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs';
import { AggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { mockDataServices, mockAggTypesRegistry } from './test_helpers';
+import { MetricAggType } from './metrics/metric_agg_type';
import { Field as IndexPatternField, IndexPattern } from '../../index_patterns';
import { stubIndexPatternWithFields } from '../../../public/stubs';
+import { FieldFormatsStart } from '../../field_formats';
import { fieldFormatsServiceMock } from '../../field_formats/mocks';
describe('AggConfig', () => {
let indexPattern: IndexPattern;
let typesRegistry: AggTypesRegistryStart;
- const fieldFormats = fieldFormatsServiceMock.createStartContract();
+ let fieldFormats: FieldFormatsStart;
beforeEach(() => {
jest.restoreAllMocks();
mockDataServices();
+ fieldFormats = fieldFormatsServiceMock.createStartContract();
indexPattern = stubIndexPatternWithFields as IndexPattern;
typesRegistry = mockAggTypesRegistry();
});
@@ -325,7 +328,7 @@ describe('AggConfig', () => {
});
});
- describe('#toJSON', () => {
+ describe('#serialize', () => {
it('includes the aggs id, params, type and schema', () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
@@ -342,7 +345,7 @@ describe('AggConfig', () => {
expect(aggConfig.type).toHaveProperty('name', 'date_histogram');
expect(typeof aggConfig.schema).toBe('string');
- const state = aggConfig.toJSON();
+ const state = aggConfig.serialize();
expect(state).toHaveProperty('id', '1');
expect(typeof state.params).toBe('object');
expect(state).toHaveProperty('type', 'date_histogram');
@@ -367,6 +370,201 @@ describe('AggConfig', () => {
});
});
+ describe('#toExpressionAst', () => {
+ beforeEach(() => {
+ fieldFormats.getDefaultInstance = (() => ({
+ getConverterFor: (t?: string) => t || identity,
+ })) as any;
+ indexPattern.fields.getByName = name =>
+ ({
+ format: {
+ getConverterFor: (t?: string) => t || identity,
+ },
+ } as IndexPatternField);
+ });
+
+ it('works with primitive param types', () => {
+ const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
+ const configStates = {
+ enabled: true,
+ type: 'terms',
+ schema: 'segment',
+ params: {
+ field: 'machine.os.keyword',
+ order: 'asc',
+ },
+ };
+ const aggConfig = ac.createAggConfig(configStates);
+ expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(`
+ Object {
+ "arguments": Object {
+ "enabled": Array [
+ true,
+ ],
+ "id": Array [
+ "1",
+ ],
+ "missingBucket": Array [
+ false,
+ ],
+ "missingBucketLabel": Array [
+ "Missing",
+ ],
+ "order": Array [
+ "asc",
+ ],
+ "otherBucket": Array [
+ false,
+ ],
+ "otherBucketLabel": Array [
+ "Other",
+ ],
+ "schema": Array [
+ "segment",
+ ],
+ "size": Array [
+ 5,
+ ],
+ },
+ "function": "aggTerms",
+ "type": "function",
+ }
+ `);
+ });
+
+ it('creates a subexpression for params of type "agg"', () => {
+ const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
+ const configStates = {
+ type: 'terms',
+ params: {
+ field: 'machine.os.keyword',
+ order: 'asc',
+ orderAgg: {
+ enabled: true,
+ type: 'terms',
+ params: {
+ field: 'bytes',
+ order: 'asc',
+ size: 5,
+ },
+ },
+ },
+ };
+ const aggConfig = ac.createAggConfig(configStates);
+ const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg;
+ expect(aggArg).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "enabled": Array [
+ true,
+ ],
+ "id": Array [
+ "1-orderAgg",
+ ],
+ "missingBucket": Array [
+ false,
+ ],
+ "missingBucketLabel": Array [
+ "Missing",
+ ],
+ "order": Array [
+ "asc",
+ ],
+ "otherBucket": Array [
+ false,
+ ],
+ "otherBucketLabel": Array [
+ "Other",
+ ],
+ "schema": Array [
+ "orderAgg",
+ ],
+ "size": Array [
+ 5,
+ ],
+ },
+ "function": "aggTerms",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ]
+ `);
+ });
+
+ it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => {
+ // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function
+ const range: MetricAggType = typesRegistry.get('range');
+ range.expressionName = 'aggRange';
+ const rangesParam = range.params.find(p => p.name === 'ranges');
+ rangesParam!.toExpressionAst = (val: any) => ({
+ type: 'function',
+ function: 'aggRanges',
+ arguments: {
+ ranges: ['oh hi there!'],
+ },
+ });
+
+ const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
+ const configStates = {
+ type: 'range',
+ params: {
+ field: 'bytes',
+ },
+ };
+
+ const aggConfig = ac.createAggConfig(configStates);
+ const ranges = aggConfig.toExpressionAst()!.arguments.ranges;
+ expect(ranges).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "ranges": Array [
+ "oh hi there!",
+ ],
+ },
+ "function": "aggRanges",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ]
+ `);
+ });
+
+ it('stringifies any other params which are an object', () => {
+ const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
+ const configStates = {
+ type: 'terms',
+ params: {
+ field: 'machine.os.keyword',
+ order: 'asc',
+ json: { foo: 'bar' },
+ },
+ };
+ const aggConfig = ac.createAggConfig(configStates);
+ const json = aggConfig.toExpressionAst()?.arguments.json;
+ expect(json).toEqual([JSON.stringify(configStates.params.json)]);
+ });
+
+ it(`returns undefined if an expressionName doesn't exist on the agg type`, () => {
+ const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
+ const configStates = {
+ type: 'unknown type',
+ params: {},
+ };
+ const aggConfig = ac.createAggConfig(configStates);
+ expect(aggConfig.toExpressionAst()).toBe(undefined);
+ });
+ });
+
describe('#makeLabel', () => {
let aggConfig: AggConfig;
@@ -422,6 +620,9 @@ describe('AggConfig', () => {
let aggConfig: AggConfig;
beforeEach(() => {
+ fieldFormats.getDefaultInstance = (() => ({
+ getConverterFor: (t?: string) => t || identity,
+ })) as any;
indexPattern.fields.getByName = name =>
({
format: {
@@ -434,11 +635,7 @@ describe('AggConfig', () => {
type: 'histogram',
schema: 'bucket',
params: {
- field: {
- format: {
- getConverterFor: (t?: string) => t || identity,
- },
- },
+ field: 'bytes',
},
};
const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats });
@@ -446,6 +643,11 @@ describe('AggConfig', () => {
});
it("returns the field's formatter", () => {
+ aggConfig.params.field = {
+ format: {
+ getConverterFor: (t?: string) => t || identity,
+ },
+ };
expect(aggConfig.fieldFormatter().toString()).toBe(
aggConfig
.getField()
diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts
index 6188849e0e6d44..973c69e3d4f5fa 100644
--- a/src/plugins/data/public/search/aggs/agg_config.ts
+++ b/src/plugins/data/public/search/aggs/agg_config.ts
@@ -19,6 +19,8 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
+import { Assign } from '@kbn/utility-types';
+import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public';
import { IAggType } from './agg_type';
import { writeParams } from './agg_params';
import { IAggConfigs } from './agg_configs';
@@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source';
import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common';
import { FieldFormatsStart } from '../../field_formats';
-export interface AggConfigOptions {
- type: IAggType;
+type State = string | number | boolean | null | undefined | SerializableState;
+
+interface SerializableState {
+ [key: string]: State | State[];
+}
+
+export interface AggConfigSerialized {
+ type: string;
enabled?: boolean;
id?: string;
- params?: Record;
+ params?: SerializableState;
schema?: string;
}
@@ -39,6 +47,8 @@ export interface AggConfigDependencies {
fieldFormats: FieldFormatsStart;
}
+export type AggConfigOptions = Assign;
+
/**
* @name AggConfig
*
@@ -257,7 +267,10 @@ export class AggConfig {
return configDsl;
}
- toJSON() {
+ /**
+ * @returns Returns a serialized representation of an AggConfig.
+ */
+ serialize(): AggConfigSerialized {
const params = this.params;
const outParams = _.transform(
@@ -281,7 +294,64 @@ export class AggConfig {
enabled: this.enabled,
type: this.type && this.type.name,
schema: this.schema,
- params: outParams,
+ params: outParams as SerializableState,
+ };
+ }
+
+ /**
+ * @deprecated - Use serialize() instead.
+ */
+ toJSON(): AggConfigSerialized {
+ return this.serialize();
+ }
+
+ /**
+ * @returns Returns an ExpressionAst representing the function for this agg type.
+ */
+ toExpressionAst(): ExpressionAstFunction | undefined {
+ const functionName = this.type && this.type.expressionName;
+ const { type, ...rest } = this.serialize();
+ if (!functionName || !rest.params) {
+ // Return undefined - there is no matching expression function for this agg
+ return;
+ }
+
+ // Go through each of the params and convert to an array of expression args.
+ const params = Object.entries(rest.params).reduce((acc, [key, value]) => {
+ const deserializedParam = this.getAggParams().find(p => p.name === key);
+
+ if (deserializedParam && deserializedParam.toExpressionAst) {
+ // If the param provides `toExpressionAst`, we call it with the value
+ const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key));
+ if (paramExpressionAst) {
+ acc[key] = [
+ {
+ type: 'expression',
+ chain: [paramExpressionAst],
+ },
+ ];
+ }
+ } else if (typeof value === 'object') {
+ // For object params which don't provide `toExpressionAst`, we stringify
+ acc[key] = [JSON.stringify(value)];
+ } else if (typeof value !== 'undefined') {
+ // Everything else just gets stored in an array if it is defined
+ acc[key] = [value];
+ }
+
+ return acc;
+ }, {} as Record);
+
+ return {
+ type: 'function',
+ function: functionName,
+ arguments: {
+ ...params,
+ // Expression args which are provided to all functions
+ id: [this.id],
+ enabled: [this.enabled],
+ ...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined
+ },
};
}
diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts
index 5ad09f824d3e4d..d2151a2c5ed4db 100644
--- a/src/plugins/data/public/search/aggs/agg_configs.ts
+++ b/src/plugins/data/public/search/aggs/agg_configs.ts
@@ -20,7 +20,7 @@
import _ from 'lodash';
import { Assign } from '@kbn/utility-types';
-import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config';
+import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config';
import { IAggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { AggGroupNames } from './agg_groups';
@@ -51,7 +51,7 @@ export interface AggConfigsOptions {
fieldFormats: FieldFormatsStart;
}
-export type CreateAggConfigParams = Assign;
+export type CreateAggConfigParams = Assign;
/**
* @name AggConfigs
diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts
index 70c116d560c6f3..fb0cb609a08cfe 100644
--- a/src/plugins/data/public/search/aggs/agg_type.ts
+++ b/src/plugins/data/public/search/aggs/agg_type.ts
@@ -39,6 +39,7 @@ export interface AggTypeConfig<
createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any;
type?: string;
dslName?: string;
+ expressionName?: string;
makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string);
ordered?: any;
hasNoDsl?: boolean;
@@ -88,6 +89,14 @@ export class AggType<
* @type {string}
*/
dslName: string;
+ /**
+ * the name of the expression function that this aggType represents.
+ * TODO: this should probably be a required field.
+ *
+ * @property name
+ * @type {string}
+ */
+ expressionName?: string;
/**
* the user friendly name that will be shown in the ui for this aggType
*
@@ -219,6 +228,7 @@ export class AggType<
this.name = config.name;
this.type = config.type || 'metrics';
this.dslName = config.dslName || config.name;
+ this.expressionName = config.expressionName;
this.title = config.title;
this.makeLabel = config.makeLabel || constant(this.name);
this.ordered = config.ordered;
diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts
index 4b154c338d48c2..da07f581c9274c 100644
--- a/src/plugins/data/public/search/aggs/agg_types.ts
+++ b/src/plugins/data/public/search/aggs/agg_types.ts
@@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative';
import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum';
import { getMovingAvgMetricAgg } from './metrics/moving_avg';
import { getSerialDiffMetricAgg } from './metrics/serial_diff';
+
import { getDateHistogramBucketAgg } from './buckets/date_histogram';
import { getHistogramBucketAgg } from './buckets/histogram';
import { getRangeBucketAgg } from './buckets/range';
@@ -103,3 +104,7 @@ export const getAggTypes = ({
getGeoTitleBucketAgg({ getInternalStartServices }),
],
});
+
+import { aggTerms } from './buckets/terms_fn';
+
+export const getAggTypesFunctions = () => [aggTerms];
diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts
index 698e0dfb1d3404..a12a1d7ac2d3d5 100644
--- a/src/plugins/data/public/search/aggs/buckets/terms.ts
+++ b/src/plugins/data/public/search/aggs/buckets/terms.ts
@@ -26,7 +26,7 @@ import {
isStringOrNumberType,
migrateIncludeExcludeFormat,
} from './migrate_include_exclude_format';
-import { IAggConfigs } from '../agg_configs';
+import { AggConfigSerialized, IAggConfigs } from '../types';
import { Adapters } from '../../../../../inspector/public';
import { ISearchSource } from '../../search_source';
@@ -63,10 +63,27 @@ export interface TermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
+export interface AggParamsTerms {
+ field: string;
+ order: 'asc' | 'desc';
+ orderBy: string;
+ orderAgg?: AggConfigSerialized;
+ size?: number;
+ missingBucket?: boolean;
+ missingBucketLabel?: string;
+ otherBucket?: boolean;
+ otherBucketLabel?: string;
+ // advanced
+ exclude?: string;
+ include?: string;
+ json?: string;
+}
+
export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.TERMS,
+ expressionName: 'aggTerms',
title: termsTitle,
makeLabel(agg) {
const params = agg.params;
@@ -154,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
- makeAgg(termsAgg, state) {
- state = state || {};
+ makeAgg(termsAgg, state = { type: 'count' }) {
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig(state, {
addToAggConfigs: false,
diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts
new file mode 100644
index 00000000000000..f55f1de7960138
--- /dev/null
+++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts
@@ -0,0 +1,164 @@
+/*
+ * 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 { functionWrapper } from '../test_helpers';
+import { aggTerms } from './terms_fn';
+
+describe('agg_expression_functions', () => {
+ describe('aggTerms', () => {
+ const fn = functionWrapper(aggTerms());
+
+ test('fills in defaults when only required args are provided', () => {
+ const actual = fn({
+ field: 'machine.os.keyword',
+ order: 'asc',
+ orderBy: '1',
+ });
+ expect(actual).toMatchInlineSnapshot(`
+ Object {
+ "type": "agg_type",
+ "value": Object {
+ "enabled": true,
+ "id": undefined,
+ "params": Object {
+ "exclude": undefined,
+ "field": "machine.os.keyword",
+ "include": undefined,
+ "json": undefined,
+ "missingBucket": false,
+ "missingBucketLabel": "Missing",
+ "order": "asc",
+ "orderAgg": undefined,
+ "orderBy": "1",
+ "otherBucket": false,
+ "otherBucketLabel": "Other",
+ "size": 5,
+ },
+ "schema": undefined,
+ "type": "terms",
+ },
+ }
+ `);
+ });
+
+ test('includes optional params when they are provided', () => {
+ const actual = fn({
+ id: '1',
+ enabled: false,
+ schema: 'whatever',
+ field: 'machine.os.keyword',
+ order: 'desc',
+ orderBy: '2',
+ size: 6,
+ missingBucket: true,
+ missingBucketLabel: 'missing',
+ otherBucket: true,
+ otherBucketLabel: 'other',
+ exclude: 'ios',
+ });
+
+ expect(actual.value).toMatchInlineSnapshot(`
+ Object {
+ "enabled": false,
+ "id": "1",
+ "params": Object {
+ "exclude": "ios",
+ "field": "machine.os.keyword",
+ "include": undefined,
+ "json": undefined,
+ "missingBucket": true,
+ "missingBucketLabel": "missing",
+ "order": "desc",
+ "orderAgg": undefined,
+ "orderBy": "2",
+ "otherBucket": true,
+ "otherBucketLabel": "other",
+ "size": 6,
+ },
+ "schema": "whatever",
+ "type": "terms",
+ }
+ `);
+ });
+
+ test('handles orderAgg as a subexpression', () => {
+ const actual = fn({
+ field: 'machine.os.keyword',
+ order: 'asc',
+ orderBy: '1',
+ orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }),
+ });
+
+ expect(actual.value.params).toMatchInlineSnapshot(`
+ Object {
+ "exclude": undefined,
+ "field": "machine.os.keyword",
+ "include": undefined,
+ "json": undefined,
+ "missingBucket": false,
+ "missingBucketLabel": "Missing",
+ "order": "asc",
+ "orderAgg": Object {
+ "enabled": true,
+ "id": undefined,
+ "params": Object {
+ "exclude": undefined,
+ "field": "name",
+ "include": undefined,
+ "json": undefined,
+ "missingBucket": false,
+ "missingBucketLabel": "Missing",
+ "order": "asc",
+ "orderAgg": undefined,
+ "orderBy": "1",
+ "otherBucket": false,
+ "otherBucketLabel": "Other",
+ "size": 5,
+ },
+ "schema": undefined,
+ "type": "terms",
+ },
+ "orderBy": "1",
+ "otherBucket": false,
+ "otherBucketLabel": "Other",
+ "size": 5,
+ }
+ `);
+ });
+
+ test('correctly parses json string argument', () => {
+ const actual = fn({
+ field: 'machine.os.keyword',
+ order: 'asc',
+ orderBy: '1',
+ json: '{ "foo": true }',
+ });
+
+ expect(actual.value.params.json).toEqual({ foo: true });
+ expect(() => {
+ fn({
+ field: 'machine.os.keyword',
+ order: 'asc',
+ orderBy: '1',
+ json: '/// intentionally malformed json ///',
+ });
+ }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
+ });
+ });
+});
diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts
new file mode 100644
index 00000000000000..7980bfabe79fb1
--- /dev/null
+++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts
@@ -0,0 +1,181 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { Assign } from '@kbn/utility-types';
+import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
+import { AggExpressionType, AggExpressionFunctionArgs } from '../';
+
+const aggName = 'terms';
+const fnName = 'aggTerms';
+
+type Input = any;
+type AggArgs = AggExpressionFunctionArgs;
+// Since the orderAgg param is an agg nested in a subexpression, we need to
+// overwrite the param type to expect a value of type AggExpressionType.
+type Arguments = AggArgs &
+ Assign<
+ AggArgs,
+ { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType }
+ >;
+type Output = AggExpressionType;
+type FunctionDefinition = ExpressionFunctionDefinition;
+
+export const aggTerms = (): FunctionDefinition => ({
+ name: fnName,
+ help: i18n.translate('data.search.aggs.function.buckets.terms.help', {
+ defaultMessage: 'Generates a serialized agg config for a terms agg',
+ }),
+ type: 'agg_type',
+ args: {
+ id: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.id.help', {
+ defaultMessage: 'ID for this aggregation',
+ }),
+ },
+ enabled: {
+ types: ['boolean'],
+ default: true,
+ help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', {
+ defaultMessage: 'Specifies whether this aggregation should be enabled',
+ }),
+ },
+ schema: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.schema.help', {
+ defaultMessage: 'Schema to use for this aggregation',
+ }),
+ },
+ field: {
+ types: ['string'],
+ required: true,
+ help: i18n.translate('data.search.aggs.buckets.terms.field.help', {
+ defaultMessage: 'Field to use for this aggregation',
+ }),
+ },
+ order: {
+ types: ['string'],
+ required: true,
+ help: i18n.translate('data.search.aggs.buckets.terms.order.help', {
+ defaultMessage: 'Order in which to return the results: asc or desc',
+ }),
+ },
+ orderBy: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', {
+ defaultMessage: 'Field to order results by',
+ }),
+ },
+ orderAgg: {
+ types: ['agg_type'],
+ help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', {
+ defaultMessage: 'Agg config to use for ordering results',
+ }),
+ },
+ size: {
+ types: ['number'],
+ default: 5,
+ help: i18n.translate('data.search.aggs.buckets.terms.size.help', {
+ defaultMessage: 'Max number of buckets to retrieve',
+ }),
+ },
+ missingBucket: {
+ types: ['boolean'],
+ default: false,
+ help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', {
+ defaultMessage: 'When set to true, groups together any buckets with missing fields',
+ }),
+ },
+ missingBucketLabel: {
+ types: ['string'],
+ default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', {
+ defaultMessage: 'Missing',
+ description: `Default label used in charts when documents are missing a field.
+ Visible when you create a chart with a terms aggregation and enable "Show missing values"`,
+ }),
+ help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', {
+ defaultMessage: 'Default label used in charts when documents are missing a field.',
+ }),
+ },
+ otherBucket: {
+ types: ['boolean'],
+ default: false,
+ help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', {
+ defaultMessage: 'When set to true, groups together any buckets beyond the allowed size',
+ }),
+ },
+ otherBucketLabel: {
+ types: ['string'],
+ default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', {
+ defaultMessage: 'Other',
+ }),
+ help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', {
+ defaultMessage: 'Default label used in charts for documents in the Other bucket',
+ }),
+ },
+ exclude: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', {
+ defaultMessage: 'Specific bucket values to exclude from results',
+ }),
+ },
+ include: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.include.help', {
+ defaultMessage: 'Specific bucket values to include in results',
+ }),
+ },
+ json: {
+ types: ['string'],
+ help: i18n.translate('data.search.aggs.buckets.terms.json.help', {
+ defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
+ }),
+ },
+ },
+ fn: (input, args) => {
+ const { id, enabled, schema, ...rest } = args;
+
+ let json;
+ try {
+ json = args.json ? JSON.parse(args.json) : undefined;
+ } catch (e) {
+ throw new Error('Unable to parse json argument string');
+ }
+
+ // Need to spread this object to work around TS bug:
+ // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742
+ const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined;
+
+ return {
+ type: 'agg_type',
+ value: {
+ id,
+ enabled,
+ schema,
+ type: aggName,
+ params: {
+ ...rest,
+ orderAgg,
+ json,
+ },
+ },
+ };
+ },
+});
diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts
index 3868d8f1bcd16c..947394c97bdcd2 100644
--- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts
+++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts
@@ -36,14 +36,14 @@ const metricAggFilter = [
'!geo_centroid',
];
-const parentPipelineType = i18n.translate(
+export const parentPipelineType = i18n.translate(
'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle',
{
defaultMessage: 'Parent Pipeline Aggregations',
}
);
-const parentPipelineAggHelper = {
+export const parentPipelineAggHelper = {
subtype: parentPipelineType,
params() {
return [
@@ -56,13 +56,9 @@ const parentPipelineAggHelper = {
name: 'customMetric',
type: 'agg',
allowedAggs: metricAggFilter,
- makeAgg(termsAgg, state: any) {
- state = state || { type: 'count' };
-
+ makeAgg(termsAgg, state = { type: 'count' }) {
const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
-
metricAgg.id = termsAgg.id + '-metric';
-
return metricAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@@ -89,5 +85,3 @@ const parentPipelineAggHelper = {
return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))();
},
};
-
-export { parentPipelineAggHelper, parentPipelineType };
diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts
index c1d05a39285b70..cee7841a8c3b98 100644
--- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts
+++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts
@@ -43,14 +43,14 @@ const metricAggFilter: string[] = [
];
const bucketAggFilter: string[] = [];
-const siblingPipelineType = i18n.translate(
+export const siblingPipelineType = i18n.translate(
'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle',
{
defaultMessage: 'Sibling pipeline aggregations',
}
);
-const siblingPipelineAggHelper = {
+export const siblingPipelineAggHelper = {
subtype: siblingPipelineType,
params() {
return [
@@ -59,11 +59,9 @@ const siblingPipelineAggHelper = {
type: 'agg',
allowedAggs: bucketAggFilter,
default: null,
- makeAgg(agg: IMetricAggConfig, state: any) {
- state = state || { type: 'date_histogram' };
+ makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) {
const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
orderAgg.id = agg.id + '-bucket';
-
return orderAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@@ -76,11 +74,9 @@ const siblingPipelineAggHelper = {
type: 'agg',
allowedAggs: metricAggFilter,
default: null,
- makeAgg(agg: IMetricAggConfig, state: any) {
- state = state || { type: 'count' };
+ makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) {
const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
orderAgg.id = agg.id + '-metric';
-
return orderAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@@ -98,5 +94,3 @@ const siblingPipelineAggHelper = {
: new (FieldFormat.from(identity))();
},
};
-
-export { siblingPipelineAggHelper, siblingPipelineType };
diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts
index d20530a17ca657..7491f15aa3002d 100644
--- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts
+++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts
@@ -25,21 +25,28 @@ import {
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
+import { FieldFormatsStart } from '../../../field_formats';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { InternalStartServices } from '../../../types';
describe('AggTypesMetricsPercentileRanksProvider class', function() {
let aggConfigs: IAggConfigs;
- const aggTypesDependencies: PercentileRanksMetricAggDependencies = {
- getInternalStartServices: () =>
- (({
- fieldFormats: fieldFormatsServiceMock.createStartContract(),
- notifications: notificationServiceMock.createStartContract(),
- } as unknown) as InternalStartServices),
- };
+ let fieldFormats: FieldFormatsStart;
+ let aggTypesDependencies: PercentileRanksMetricAggDependencies;
beforeEach(() => {
+ fieldFormats = fieldFormatsServiceMock.createStartContract();
+ fieldFormats.getDefaultInstance = (() => ({
+ convert: (t?: string) => t,
+ })) as any;
+ aggTypesDependencies = {
+ getInternalStartServices: () =>
+ (({
+ fieldFormats,
+ notifications: notificationServiceMock.createStartContract(),
+ } as unknown) as InternalStartServices),
+ };
const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]);
const field = {
name: 'bytes',
@@ -61,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() {
type: METRIC_TYPES.PERCENTILE_RANKS,
schema: 'metric',
params: {
- field: {
- displayName: 'bytes',
- format: {
- convert: jest.fn(x => x),
- },
- },
+ field: 'bytes',
customLabel: 'my custom field label',
values: [5000, 10000],
},
diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts
index 0ac1e8417514c7..76382c01bcc108 100644
--- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts
+++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts
@@ -61,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => {
type: METRIC_TYPES.PERCENTILES,
schema: 'metric',
params: {
- field: {
- displayName: 'bytes',
- format: {
- convert: jest.fn(x => `${x}th`),
- },
- },
+ field: 'bytes',
customLabel: 'prince',
percents: [95],
},
diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/public/search/aggs/param_types/agg.ts
index e5b53020c3159b..e3f8c7c922170f 100644
--- a/src/plugins/data/public/search/aggs/param_types/agg.ts
+++ b/src/plugins/data/public/search/aggs/param_types/agg.ts
@@ -17,13 +17,13 @@
* under the License.
*/
-import { AggConfig, IAggConfig } from '../agg_config';
+import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config';
import { BaseParamType } from './base';
export class AggParamType extends BaseParamType<
TAggConfig
> {
- makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
+ makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
allowedAggs: string[] = [];
constructor(config: Record) {
@@ -42,17 +42,25 @@ export class AggParamType extends Ba
}
if (!config.serialize) {
this.serialize = (agg: TAggConfig) => {
- return agg.toJSON();
+ return agg.serialize();
};
}
if (!config.deserialize) {
- this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => {
+ this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => {
if (!agg) {
throw new Error('aggConfig was not provided to AggParamType deserialize function');
}
return this.makeAgg(agg, state);
};
}
+ if (!config.toExpressionAst) {
+ this.toExpressionAst = (agg: TAggConfig) => {
+ if (!agg || !agg.toExpressionAst) {
+ throw new Error('aggConfig was not provided to AggParamType toExpressionAst function');
+ }
+ return agg.toExpressionAst();
+ };
+ }
this.makeAgg = config.makeAgg;
this.valueType = AggConfig;
diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts
index 2cbc5866e284db..a6f7e5adea0436 100644
--- a/src/plugins/data/public/search/aggs/param_types/base.ts
+++ b/src/plugins/data/public/search/aggs/param_types/base.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { ExpressionAstFunction } from 'src/plugins/expressions/public';
import { IAggConfigs } from '../agg_configs';
import { IAggConfig } from '../agg_config';
import { FetchOptions } from '../../fetch';
@@ -37,6 +38,7 @@ export class BaseParamType {
) => void;
serialize: (value: any, aggConfig?: TAggConfig) => any;
deserialize: (value: any, aggConfig?: TAggConfig) => any;
+ toExpressionAst?: (value: any) => ExpressionAstFunction | undefined;
options: any[];
valueType?: any;
@@ -77,6 +79,7 @@ export class BaseParamType {
this.write = config.write || defaultWrite;
this.serialize = config.serialize;
this.deserialize = config.deserialize;
+ this.toExpressionAst = config.toExpressionAst;
this.options = config.options;
this.modifyAggConfigOnSearchRequestStart =
config.modifyAggConfigOnSearchRequestStart || function() {};
diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts
new file mode 100644
index 00000000000000..cb0e37c0296d78
--- /dev/null
+++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { mapValues } from 'lodash';
+import {
+ AnyExpressionFunctionDefinition,
+ ExpressionFunctionDefinition,
+ ExecutionContext,
+} from '../../../../../../plugins/expressions/public';
+
+/**
+ * Takes a function spec and passes in default args,
+ * overriding with any provided args.
+ *
+ * Similar to the test helper used in Expressions & Canvas,
+ * however in this case we are ignoring the input & execution
+ * context, as they are not applicable to the agg type
+ * expression functions.
+ */
+export const functionWrapper = (spec: T) => {
+ const defaultArgs = mapValues(spec.args, argSpec => argSpec.default);
+ return (
+ args: T extends ExpressionFunctionDefinition<
+ infer Name,
+ infer Input,
+ infer Arguments,
+ infer Output,
+ infer Context
+ >
+ ? Arguments
+ : never
+ ) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext);
+};
diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts
index 131f921586144c..63f8ae0ce5f581 100644
--- a/src/plugins/data/public/search/aggs/test_helpers/index.ts
+++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts
@@ -17,5 +17,6 @@
* under the License.
*/
+export { functionWrapper } from './function_wrapper';
export { mockAggTypesRegistry } from './mock_agg_types_registry';
export { mockDataServices } from './mock_data_services';
diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts
index 4b2b1620ad1d38..95a7a450135670 100644
--- a/src/plugins/data/public/search/aggs/types.ts
+++ b/src/plugins/data/public/search/aggs/types.ts
@@ -19,21 +19,23 @@
import { IndexPattern } from '../../index_patterns';
import {
+ AggConfig,
+ AggConfigSerialized,
+ AggConfigs,
+ AggParamsTerms,
AggType,
+ aggTypeFieldFilters,
AggTypesRegistrySetup,
AggTypesRegistryStart,
- AggConfig,
- AggConfigs,
CreateAggConfigParams,
FieldParamType,
getCalculateAutoTimeExpression,
MetricAggType,
- aggTypeFieldFilters,
parentPipelineAggHelper,
siblingPipelineAggHelper,
} from './';
-export { IAggConfig } from './agg_config';
+export { IAggConfig, AggConfigSerialized } from './agg_config';
export { CreateAggConfigParams, IAggConfigs } from './agg_configs';
export { IAggType } from './agg_type';
export { AggParam, AggParamOption } from './agg_params';
@@ -70,3 +72,25 @@ export interface SearchAggsStart {
) => InstanceType;
types: AggTypesRegistryStart;
}
+
+/** @internal */
+export interface AggExpressionType {
+ type: 'agg_type';
+ value: AggConfigSerialized;
+}
+
+/** @internal */
+export type AggExpressionFunctionArgs<
+ Name extends keyof AggParamsMapping
+> = AggParamsMapping[Name] & Pick;
+
+/**
+ * A global list of the param interfaces for each agg type.
+ * For now this is internal, but eventually we will probably
+ * want to make it public.
+ *
+ * @internal
+ */
+export interface AggParamsMapping {
+ terms: AggParamsTerms;
+}
diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts
index 4ca976d328c919..78b4935077d103 100644
--- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts
+++ b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts
@@ -27,7 +27,7 @@ export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumn
return {
type: aggConfig.type.name,
indexPatternId: aggConfig.getIndexPattern().id,
- aggConfigParams: aggConfig.toJSON().params,
+ aggConfigParams: aggConfig.serialize().params,
};
};
diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts
index 19308dd387d3aa..b1f7925bec4bbd 100644
--- a/src/plugins/data/public/search/search_service.test.ts
+++ b/src/plugins/data/public/search/search_service.test.ts
@@ -18,9 +18,10 @@
*/
import { coreMock } from '../../../../core/public/mocks';
+import { CoreSetup } from '../../../../core/public';
+import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks';
import { SearchService } from './search_service';
-import { CoreSetup } from '../../../../core/public';
describe('Search service', () => {
let searchService: SearchService;
@@ -35,6 +36,7 @@ describe('Search service', () => {
it('exposes proper contract', async () => {
const setup = searchService.setup(mockCoreSetup, {
packageInfo: { version: '8' },
+ expressions: expressionsPluginMock.createSetupContract(),
} as any);
expect(setup).toHaveProperty('registerSearchStrategyProvider');
});
diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts
index 3f3266b5fe90ff..b59524baa9fa79 100644
--- a/src/plugins/data/public/search/search_service.ts
+++ b/src/plugins/data/public/search/search_service.ts
@@ -18,6 +18,7 @@
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public';
+import { ExpressionsSetup } from '../../../../plugins/expressions/public';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import {
@@ -37,6 +38,7 @@ import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
import {
getAggTypes,
+ getAggTypesFunctions,
AggType,
AggTypesRegistry,
AggConfig,
@@ -52,9 +54,10 @@ import { FieldFormatsStart } from '../field_formats';
import { ISearchGeneric } from './i_search';
interface SearchServiceSetupDependencies {
+ expressions: ExpressionsSetup;
+ getInternalStartServices: GetInternalStartServicesFn;
packageInfo: PackageInfo;
query: QuerySetup;
- getInternalStartServices: GetInternalStartServicesFn;
}
interface SearchServiceStartDependencies {
@@ -97,22 +100,27 @@ export class SearchService implements Plugin {
public setup(
core: CoreSetup,
- { packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
+ { expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
): ISearchSetup {
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider);
this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider);
const aggTypesSetup = this.aggTypesRegistry.setup();
+
+ // register each agg type
const aggTypes = getAggTypes({
query,
uiSettings: core.uiSettings,
getInternalStartServices,
});
-
aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b));
aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m));
+ // register expression functions for each agg type
+ const aggFunctions = getAggTypesFunctions();
+ aggFunctions.forEach(fn => expressions.registerFunction(fn));
+
return {
aggs: {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),
diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json
index d5b047924f599e..1c4b44a946e62b 100644
--- a/src/plugins/home/kibana.json
+++ b/src/plugins/home/kibana.json
@@ -4,5 +4,5 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "kibanaLegacy"],
- "optionalPlugins": ["usage_collection", "telemetry"]
+ "optionalPlugins": ["usageCollection", "telemetry"]
}
diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts
index e48ed1403d9ce6..1050c19362ae17 100644
--- a/src/plugins/home/server/plugin.ts
+++ b/src/plugins/home/server/plugin.ts
@@ -30,7 +30,7 @@ import { capabilitiesProvider } from './capabilities_provider';
import { sampleDataTelemetry } from './saved_objects';
interface HomeServerPluginSetupDependencies {
- usage_collection?: UsageCollectionSetup;
+ usageCollection?: UsageCollectionSetup;
}
export class HomeServerPlugin implements Plugin {
@@ -43,7 +43,7 @@ export class HomeServerPlugin implements Plugin;
+}
export function mockTelemetryService({
reportOptInStatusChange,
-}: { reportOptInStatusChange?: boolean } = {}) {
+ config: configOverride = {},
+}: TelemetryServiceMockOptions = {}) {
const config = {
enabled: true,
url: 'http://localhost',
@@ -39,14 +48,22 @@ export function mockTelemetryService({
banner: true,
allowChangingOptInStatus: true,
telemetryNotifyUserAboutOptInDefault: true,
+ ...configOverride,
};
- return new TelemetryService({
+ const telemetryService = new TelemetryService({
config,
http: httpServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
reportOptInStatusChange,
});
+
+ const originalReportOptInStatus = telemetryService['reportOptInStatus'];
+ telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(optInPayload => {
+ return originalReportOptInStatus(optInPayload); // Actually calling the original method
+ });
+
+ return telemetryService;
}
export function mockTelemetryNotifications({
diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts
index 0a49b0ae3084e3..16faa0cfc7536b 100644
--- a/src/plugins/telemetry/public/services/telemetry_service.test.ts
+++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts
@@ -67,17 +67,31 @@ describe('TelemetryService', () => {
});
describe('setOptIn', () => {
+ it('does not call the api if canChangeOptInStatus==false', async () => {
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: false },
+ });
+ expect(await telemetryService.setOptIn(true)).toBe(false);
+
+ expect(telemetryService['http'].post).toBeCalledTimes(0);
+ });
+
it('calls api if canChangeOptInStatus', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: true },
+ });
await telemetryService.setOptIn(true);
expect(telemetryService['http'].post).toBeCalledTimes(1);
});
it('sends enabled true if optedIn: true', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: true },
+ });
const optedIn = true;
await telemetryService.setOptIn(optedIn);
@@ -87,8 +101,10 @@ describe('TelemetryService', () => {
});
it('sends enabled false if optedIn: false', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: true },
+ });
const optedIn = false;
await telemetryService.setOptIn(optedIn);
@@ -98,9 +114,10 @@ describe('TelemetryService', () => {
});
it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
- telemetryService['reportOptInStatus'] = jest.fn();
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: true },
+ });
await telemetryService.setOptIn(true);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0);
@@ -108,9 +125,10 @@ describe('TelemetryService', () => {
});
it('calls reportOptInStatus if reportOptInStatusChange is true', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: true });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
- telemetryService['reportOptInStatus'] = jest.fn();
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: true,
+ config: { allowChangingOptInStatus: true },
+ });
await telemetryService.setOptIn(true);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1);
@@ -118,9 +136,10 @@ describe('TelemetryService', () => {
});
it('adds an error toast on api error', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
- telemetryService['reportOptInStatus'] = jest.fn();
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: false,
+ config: { allowChangingOptInStatus: true },
+ });
telemetryService['http'].post = jest.fn().mockImplementation((url: string) => {
if (url === '/api/telemetry/v2/optIn') {
throw Error('failed to update opt in.');
@@ -133,9 +152,13 @@ describe('TelemetryService', () => {
expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1);
});
+ // This one should not happen because the entire method is fully caught but hey! :)
it('adds an error toast on reportOptInStatus error', async () => {
- const telemetryService = mockTelemetryService({ reportOptInStatusChange: true });
- telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
+ const telemetryService = mockTelemetryService({
+ reportOptInStatusChange: true,
+ config: { allowChangingOptInStatus: true },
+ });
+
telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => {
throw Error('failed to report OptIn Status.');
});
@@ -146,4 +169,50 @@ describe('TelemetryService', () => {
expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1);
});
});
+
+ describe('getTelemetryUrl', () => {
+ it('should return the config.url parameter', async () => {
+ const url = 'http://test.com';
+ const telemetryService = mockTelemetryService({
+ config: { url },
+ });
+
+ expect(telemetryService.getTelemetryUrl()).toBe(url);
+ });
+ });
+
+ describe('setUserHasSeenNotice', () => {
+ it('should hit the API and change the config', async () => {
+ const telemetryService = mockTelemetryService({
+ config: { telemetryNotifyUserAboutOptInDefault: undefined },
+ });
+
+ expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined);
+ expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false);
+ await telemetryService.setUserHasSeenNotice();
+ expect(telemetryService['http'].put).toBeCalledTimes(1);
+ expect(telemetryService.userHasSeenOptedInNotice).toBe(true);
+ expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(true);
+ });
+
+ it('should show a toast notification if the request fail', async () => {
+ const telemetryService = mockTelemetryService({
+ config: { telemetryNotifyUserAboutOptInDefault: undefined },
+ });
+
+ telemetryService['http'].put = jest.fn().mockImplementation((url: string) => {
+ if (url === '/api/telemetry/v2/userHasSeenNotice') {
+ throw Error('failed to update opt in.');
+ }
+ });
+
+ expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined);
+ expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false);
+ await telemetryService.setUserHasSeenNotice();
+ expect(telemetryService['http'].put).toBeCalledTimes(1);
+ expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1);
+ expect(telemetryService.userHasSeenOptedInNotice).toBe(false);
+ expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false);
+ });
+ });
});
diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts
index cac4e3fdf5f506..6d87a74197fe57 100644
--- a/src/plugins/telemetry/public/services/telemetry_service.ts
+++ b/src/plugins/telemetry/public/services/telemetry_service.ts
@@ -122,11 +122,15 @@ export class TelemetryService {
}
try {
- await this.http.post('/api/telemetry/v2/optIn', {
+ // Report the option to the Kibana server to store the settings.
+ // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}]
+ const optInPayload = await this.http.post('/api/telemetry/v2/optIn', {
body: JSON.stringify({ enabled: optedIn }),
});
if (this.reportOptInStatusChange) {
- await this.reportOptInStatus(optedIn);
+ // Use the response to report about the change to the remote telemetry cluster.
+ // If it's opt-out, this will be the last communication to the remote service.
+ await this.reportOptInStatus(optInPayload);
}
this.isOptedIn = optedIn;
} catch (err) {
@@ -162,7 +166,11 @@ export class TelemetryService {
}
};
- private reportOptInStatus = async (OptInStatus: boolean): Promise => {
+ /**
+ * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service
+ * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings
+ */
+ private reportOptInStatus = async (optInPayload: string[]): Promise => {
const telemetryOptInStatusUrl = this.getOptInStatusUrl();
try {
@@ -171,7 +179,7 @@ export class TelemetryService {
headers: {
'Content-Type': 'application/json',
},
- body: JSON.stringify({ enabled: OptInStatus }),
+ body: JSON.stringify(optInPayload),
});
} catch (err) {
// Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails.
diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
index e65ade0ab8aaa9..4ed5dbf2512759 100644
--- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
@@ -22,7 +22,10 @@ import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'kibana/server';
-import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
+import {
+ StatsGetterConfig,
+ TelemetryCollectionManagerPluginSetup,
+} from 'src/plugins/telemetry_collection_manager/server';
import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
@@ -79,23 +82,30 @@ export function registerTelemetryOptInRoutes({
});
}
+ const statsGetterConfig: StatsGetterConfig = {
+ start: moment()
+ .subtract(20, 'minutes')
+ .toISOString(),
+ end: moment().toISOString(),
+ unencrypted: false,
+ };
+
+ const optInStatus = await telemetryCollectionManager.getOptInStats(
+ newOptInStatus,
+ statsGetterConfig
+ );
+
if (config.sendUsageFrom === 'server') {
const optInStatusUrl = config.optInStatusUrl;
await sendTelemetryOptInStatus(
telemetryCollectionManager,
{ optInStatusUrl, newOptInStatus },
- {
- start: moment()
- .subtract(20, 'minutes')
- .toISOString(),
- end: moment().toISOString(),
- unencrypted: false,
- }
+ statsGetterConfig
);
}
await updateTelemetrySavedObject(context.core.savedObjects.client, attributes);
- return res.ok({});
+ return res.ok({ body: optInStatus });
}
);
}
diff --git a/src/plugins/vis_type_tagcloud/config.ts b/src/plugins/vis_type_tagcloud/config.ts
new file mode 100644
index 00000000000000..6749bd83de39f9
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/config.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { schema, TypeOf } from '@kbn/config-schema';
+
+export const configSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+});
+
+export type ConfigSchema = TypeOf;
diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json
new file mode 100644
index 00000000000000..dbc9a1b9ef6926
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "visTypeTagcloud",
+ "version": "kibana",
+ "ui": true,
+ "server": true,
+ "requiredPlugins": ["data", "expressions", "visualizations", "charts"]
+}
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
rename to src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/_tag_cloud.scss
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss
rename to src/plugins/vis_type_tagcloud/public/_tag_cloud.scss
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js
rename to src/plugins/vis_type_tagcloud/public/components/feedback_message.js
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js
rename to src/plugins/vis_type_tagcloud/public/components/label.js
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js
rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
similarity index 91%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
index 7a64549edd747f..d33576e4e55294 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
@@ -20,9 +20,9 @@
import React from 'react';
import { EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
-import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public';
-import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public';
+import { VisOptionsProps } from '../../../vis_default_editor/public';
+import { ValidatedDualRange } from '../../../kibana_react/public';
+import { SelectOption, SwitchOption } from '../../../charts/public';
import { TagCloudVisParams } from '../types';
function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) {
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss b/src/plugins/vis_type_tagcloud/public/index.scss
similarity index 76%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/index.scss
rename to src/plugins/vis_type_tagcloud/public/index.scss
index a4fcf8418ce1cd..e6893b9a2474c3 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss
+++ b/src/plugins/vis_type_tagcloud/public/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// Prefix all styles with "tgc" to avoid conflicts.
// Examples
// tgcChart
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts b/src/plugins/vis_type_tagcloud/public/index.ts
similarity index 93%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/index.ts
rename to src/plugins/vis_type_tagcloud/public/index.ts
index 90e6305262caa4..ff27d96b710fa9 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts
+++ b/src/plugins/vis_type_tagcloud/public/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { PluginInitializerContext } from '../../../../core/public';
+import { PluginInitializerContext } from 'kibana/public';
import { TagCloudPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts
similarity index 81%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts
rename to src/plugins/vis_type_tagcloud/public/plugin.ts
index 1061271aa315b9..6978186058b1d6 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts
+++ b/src/plugins/vis_type_tagcloud/public/plugin.ts
@@ -17,15 +17,18 @@
* under the License.
*/
-import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public';
-import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public';
-import { VisualizationsSetup } from '../../../../plugins/visualizations/public';
-import { ChartsPluginSetup } from '../../../../plugins/charts/public';
+import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public';
+import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
+import { VisualizationsSetup } from '../../visualizations/public';
+import { ChartsPluginSetup } from '../../charts/public';
import { createTagCloudFn } from './tag_cloud_fn';
import { createTagCloudVisTypeDefinition } from './tag_cloud_type';
-import { DataPublicPluginStart } from '../../../../plugins/data/public';
+import { DataPublicPluginStart } from '../../data/public';
import { setFormatService } from './services';
+import { ConfigSchema } from '../config';
+
+import './index.scss';
/** @internal */
export interface TagCloudPluginSetupDependencies {
@@ -46,9 +49,9 @@ export interface TagCloudVisPluginStartDependencies {
/** @internal */
export class TagCloudPlugin implements Plugin {
- initializerContext: PluginInitializerContext;
+ initializerContext: PluginInitializerContext;
- constructor(initializerContext: PluginInitializerContext) {
+ constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/plugins/vis_type_tagcloud/public/services.ts
similarity index 82%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/services.ts
rename to src/plugins/vis_type_tagcloud/public/services.ts
index 272bed3e91a08c..f6002afc66493d 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts
+++ b/src/plugins/vis_type_tagcloud/public/services.ts
@@ -17,11 +17,9 @@
* under the License.
*/
-import { createGetterSetter } from '../../../../plugins/kibana_utils/public';
-import { DataPublicPluginStart } from '../../../../plugins/data/public';
+import { createGetterSetter } from '../../kibana_utils/public';
+import { DataPublicPluginStart } from '../../data/public';
export const [getFormatService, setFormatService] = createGetterSetter<
DataPublicPluginStart['fieldFormats']
>('data.fieldFormats');
-
-export { npStart } from 'ui/new_platform';
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
similarity index 91%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
index 65c54766133d11..eb16b0855a138d 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
@@ -19,8 +19,7 @@
import { createTagCloudFn } from './tag_cloud_fn';
-// eslint-disable-next-line
-import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
+import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
describe('interpreter/functions#tagcloud', () => {
const fn = functionWrapper(createTagCloudFn());
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
similarity index 96%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
index 31c7fd118cefd5..05cf05ab00b759 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
@@ -19,11 +19,7 @@
import { i18n } from '@kbn/i18n';
-import {
- ExpressionFunctionDefinition,
- KibanaDatatable,
- Render,
-} from '../../../../plugins/expressions/public';
+import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public';
import { TagCloudVisParams } from './types';
const name = 'tagcloud';
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
similarity index 98%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts
rename to src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
index b7dfa62c93fb97..5a8cc3004a3154 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
@@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
-import { Schemas } from '../../../../plugins/vis_default_editor/public';
+import { Schemas } from '../../vis_default_editor/public';
import { TagCloudOptions } from './components/tag_cloud_options';
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts
similarity index 100%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/types.ts
rename to src/plugins/vis_type_tagcloud/public/types.ts
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts b/src/plugins/vis_type_tagcloud/server/index.ts
similarity index 57%
rename from src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts
rename to src/plugins/vis_type_tagcloud/server/index.ts
index f70789edc66bac..bd9656b29c5249 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts
+++ b/src/plugins/vis_type_tagcloud/server/index.ts
@@ -17,18 +17,18 @@
* under the License.
*/
-import { PluginInitializerContext } from 'kibana/public';
-import { npSetup, npStart } from 'ui/new_platform';
-import { TagCloudPluginSetupDependencies } from './plugin';
-import { plugin } from '.';
+import { PluginConfigDescriptor } from 'kibana/server';
-const plugins: Readonly = {
- expressions: npSetup.plugins.expressions,
- visualizations: npSetup.plugins.visualizations,
- charts: npSetup.plugins.charts,
-};
+import { configSchema, ConfigSchema } from '../config';
-const pluginInstance = plugin({} as PluginInitializerContext);
+export const config: PluginConfigDescriptor = {
+ schema: configSchema,
+ deprecations: ({ renameFromRoot }) => [
+ renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'),
+ ],
+};
-export const setup = pluginInstance.setup(npSetup.core, plugins);
-export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data });
+export const plugin = () => ({
+ setup() {},
+ start() {},
+});
diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts
index 7df420e7ba585f..e475684ed5934d 100644
--- a/src/plugins/visualizations/public/index.ts
+++ b/src/plugins/visualizations/public/index.ts
@@ -43,6 +43,8 @@ export type VisualizeEmbeddableContract = PublicContract;
export { VisualizeInput } from './embeddable';
export type ExprVis = ExprVisClass;
export { SchemaConfig } from './legacy/build_pipeline';
+// @ts-ignore
+export { updateOldState } from './legacy/vis_update_state';
export { PersistedState } from './persisted_state';
export {
VisualizationController,
diff --git a/src/plugins/visualizations/public/legacy/vis_update.js b/src/plugins/visualizations/public/legacy/vis_update.js
deleted file mode 100644
index 338a322e6aa574..00000000000000
--- a/src/plugins/visualizations/public/legacy/vis_update.js
+++ /dev/null
@@ -1,57 +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.
- */
-
-// TODO: this should be moved to vis_update_state
-// Currently the migration takes place in Vis when calling setCurrentState.
-// It should rather convert the raw saved object before starting to instantiate
-// any JavaScript classes from it.
-const updateVisualizationConfig = (stateConfig, config) => {
- if (!stateConfig || stateConfig.seriesParams) return;
- if (!['line', 'area', 'histogram'].includes(config.type)) return;
-
- // update value axis options
- const isUserDefinedYAxis = config.setYExtents;
- const defaultYExtents = config.defaultYExtents;
- const mode = ['stacked', 'overlap'].includes(config.mode) ? 'normal' : config.mode || 'normal';
- config.valueAxes[0].scale = {
- ...config.valueAxes[0].scale,
- type: config.scale || 'linear',
- setYExtents: config.setYExtents || false,
- defaultYExtents: config.defaultYExtents || false,
- boundsMargin: defaultYExtents ? config.boundsMargin : 0,
- min: isUserDefinedYAxis ? config.yAxis.min : undefined,
- max: isUserDefinedYAxis ? config.yAxis.max : undefined,
- mode: mode,
- };
-
- // update series options
- const interpolate = config.smoothLines ? 'cardinal' : config.interpolate;
- const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(config.mode);
- config.seriesParams[0] = {
- ...config.seriesParams[0],
- type: config.type || 'line',
- mode: stacked ? 'stacked' : 'normal',
- interpolate: interpolate,
- drawLinesBetweenPoints: config.drawLinesBetweenPoints,
- showCircles: config.showCircles,
- radiusRatio: config.radiusRatio,
- };
-};
-
-export { updateVisualizationConfig };
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js
index 45610701e08c8c..e345b9e5b8c9ab 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.js
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.js
@@ -75,6 +75,77 @@ function convertDateHistogramScaleMetrics(visState) {
}
}
+function convertSeriesParams(visState) {
+ if (visState.params.seriesParams) {
+ return;
+ }
+
+ // update value axis options
+ const isUserDefinedYAxis = visState.params.setYExtents;
+ const defaultYExtents = visState.params.defaultYExtents;
+ const mode = ['stacked', 'overlap'].includes(visState.params.mode)
+ ? 'normal'
+ : visState.params.mode || 'normal';
+
+ if (!visState.params.valueAxes || !visState.params.valueAxes.length) {
+ visState.params.valueAxes = [
+ {
+ id: 'ValueAxis-1',
+ name: 'LeftAxis-1',
+ type: 'value',
+ position: 'left',
+ show: true,
+ style: {},
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ },
+ labels: {
+ show: true,
+ rotate: 0,
+ filter: false,
+ truncate: 100,
+ },
+ title: {
+ text: 'Count',
+ },
+ },
+ ];
+ }
+
+ visState.params.valueAxes[0].scale = {
+ ...visState.params.valueAxes[0].scale,
+ type: visState.params.scale || 'linear',
+ setYExtents: visState.params.setYExtents || false,
+ defaultYExtents: visState.params.defaultYExtents || false,
+ boundsMargin: defaultYExtents ? visState.params.boundsMargin : 0,
+ min: isUserDefinedYAxis ? visState.params.yAxis.min : undefined,
+ max: isUserDefinedYAxis ? visState.params.yAxis.max : undefined,
+ mode: mode,
+ };
+
+ // update series options
+ const interpolate = visState.params.smoothLines ? 'cardinal' : visState.params.interpolate;
+ const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(visState.params.mode);
+ visState.params.seriesParams = [
+ {
+ show: true,
+ type: visState.params.type || 'line',
+ mode: stacked ? 'stacked' : 'normal',
+ interpolate: interpolate,
+ drawLinesBetweenPoints: visState.params.drawLinesBetweenPoints,
+ showCircles: visState.params.showCircles,
+ radiusRatio: visState.params.radiusRatio,
+ data: {
+ label: 'Count',
+ id: '1',
+ },
+ lineWidth: 2,
+ valueAxis: 'ValueAxis-1',
+ },
+ ];
+}
+
/**
* This function is responsible for updating old visStates - the actual saved object
* object - into the format, that will be required by the current Kibana version.
@@ -90,6 +161,10 @@ export const updateOldState = visState => {
convertPropertyNames(newState);
convertDateHistogramScaleMetrics(newState);
+ if (visState.params && ['line', 'area', 'histogram'].includes(visState.params.type)) {
+ convertSeriesParams(newState);
+ }
+
if (visState.type === 'gauge' && visState.fontSize) {
delete newState.fontSize;
_.set(newState, 'gauge.style.fontSize', visState.fontSize);
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index 3cab4faf2a27fb..009dd71b9a912e 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -29,8 +29,6 @@
import { isFunction, defaults, cloneDeep } from 'lodash';
import { PersistedState } from './persisted_state';
-// @ts-ignore
-import { updateVisualizationConfig } from './legacy/vis_update';
import { getTypes, getAggs } from './services';
import { VisType } from './vis_types';
import {
@@ -121,9 +119,6 @@ export class Vis {
this.params = this.getParams(state.params);
}
- // move to migration script
- updateVisualizationConfig(state.params, this.params);
-
if (state.data && state.data.searchSource) {
this.data.searchSource = state.data.searchSource!;
this.data.indexPattern = this.data.searchSource.getField('index');
diff --git a/tasks/config/karma.js b/tasks/config/karma.js
index 4e106ef3e039ab..1ec7c831b48641 100644
--- a/tasks/config/karma.js
+++ b/tasks/config/karma.js
@@ -53,6 +53,8 @@ module.exports = function(grunt) {
function getKarmaFiles(shardNum) {
return [
'http://localhost:5610/test_bundle/built_css.css',
+ // Sets global variables normally set by the bootstrap.js script
+ 'http://localhost:5610/test_bundle/karma/globals.js',
...UiSharedDeps.jsDepFilenames.map(
chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}`
diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts
index 086b13ecee2b3b..0168626d4a1a97 100644
--- a/test/accessibility/apps/discover.ts
+++ b/test/accessibility/apps/discover.ts
@@ -33,7 +33,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
['geo.src', 'IN'],
];
- describe('Discover', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/62497
+ describe.skip('Discover', () => {
before(async () => {
await esArchiver.load('discover');
await esArchiver.loadIfNeeded('logstash_functional');
diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js
index 3068cdd0daa5b8..af5ace8e3cd3b7 100644
--- a/x-pack/dev-tools/jest/create_jest_config.js
+++ b/x-pack/dev-tools/jest/create_jest_config.js
@@ -32,6 +32,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
'^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`,
'^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`,
'^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`,
+ '^(!!)?file-loader!': fileMockPath,
},
coverageDirectory: '/../target/kibana-coverage/jest',
coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'],
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
index d844ac8b5988dd..e0a01e9422c85b 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
@@ -191,7 +191,7 @@ describe('waterfall_helpers', () => {
name: 'SELECT FROM products',
id: 'mySpanIdB'
},
- child_ids: ['mySpanIdA', 'mySpanIdC']
+ child: { id: ['mySpanIdA', 'mySpanIdC'] }
} as Span,
{
parent: { id: 'mySpanIdD' },
@@ -294,7 +294,7 @@ describe('waterfall_helpers', () => {
name: 'SELECT FROM products',
id: 'mySpanIdB'
},
- child_ids: ['incorrectId', 'mySpanIdC']
+ child: { id: ['incorrectId', 'mySpanIdC'] }
} as Span,
{
parent: { id: 'mySpanIdD' },
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
index 8a873b2ddf1c91..73193cc7c9dbbf 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
@@ -237,7 +237,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) =>
});
/**
- * Changes the parent_id of items based on the child_ids property.
+ * Changes the parent_id of items based on the child.id property.
* Solves the problem of Inferred spans that are created as child of trace spans
* when it actually should be its parent.
* @param waterfallItems
@@ -245,10 +245,10 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) =>
const reparentSpans = (waterfallItems: IWaterfallItem[]) => {
return waterfallItems.map(waterfallItem => {
if (waterfallItem.docType === 'span') {
- const { child_ids: childIds } = waterfallItem.doc;
- if (childIds) {
- childIds.forEach(childId => {
- const item = waterfallItems.find(_item => _item.id === childId);
+ const childId = waterfallItem.doc.child?.id;
+ if (childId) {
+ childId.forEach(id => {
+ const item = waterfallItems.find(_item => _item.id === id);
if (item) {
item.parentId = waterfallItem.id;
}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
index 306c8e4f3fedbc..2f28e37f73f624 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
@@ -2027,7 +2027,7 @@ export const inferredSpans = {
id: '41226ae63af4f235',
type: 'unknown'
},
- child_ids: ['8d80de06aa11a6fc']
+ child: { ids: ['8d80de06aa11a6fc'] }
},
{
container: {
diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts
index b05379df6b0b1f..0780ab46cd873b 100644
--- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts
+++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import 'core-js/stable';
+import 'regenerator-runtime/runtime';
import 'whatwg-fetch';
-import 'babel-polyfill';
export * from './shareable';
diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js
index 8546e3712c7630..d1e8892fa2c984 100644
--- a/x-pack/legacy/plugins/maps/index.js
+++ b/x-pack/legacy/plugins/maps/index.js
@@ -57,7 +57,6 @@ export function maps(kibana) {
tilemap: _.get(mapConfig, 'tilemap', []),
};
},
- embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'],
styleSheetPaths: `${__dirname}/public/index.scss`,
savedObjectSchemas: {
'maps-telemetry': {
diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts
deleted file mode 100644
index 90b17412377f52..00000000000000
--- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-/*
- Maintain legacy embeddable legacy present while apps switch over
- */
-
-import { npSetup, npStart } from 'ui/new_platform';
-import {
- bindSetupCoreAndPlugins,
- bindStartCoreAndPlugins,
- // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../../../../plugins/maps/public/plugin';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { MAP_SAVED_OBJECT_TYPE } from '../../../../../plugins/maps/common/constants';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { MapEmbeddableFactory } from '../../../../../plugins/maps/public/embeddable';
-
-bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins);
-bindStartCoreAndPlugins(npStart.core, npStart.plugins);
-
-export * from '../../../../../plugins/maps/public/embeddable/map_embeddable_factory';
-
-npSetup.plugins.embeddable.registerEmbeddableFactory(
- MAP_SAVED_OBJECT_TYPE,
- new MapEmbeddableFactory()
-);
diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
index acfe2ada5b68d1..6328789d03f29f 100644
--- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
@@ -7,8 +7,7 @@
import React from 'react';
import { EmptyStateComponent } from '../empty_state';
import { StatesIndexStatus } from '../../../../../common/runtime_types';
-import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http';
-import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error';
+import { HttpFetchError, IHttpFetchError } from 'src/core/public';
import { mountWithRouter, shallowWithRouter } from '../../../../lib';
describe('EmptyState component', () => {
diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts
index 66b376c3ac36f4..ff9fcd05732571 100644
--- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts
@@ -6,7 +6,7 @@
import { fetchSnapshotCount } from '../snapshot';
import { apiService } from '../utils';
-import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error';
+import { HttpFetchError } from 'src/core/public';
describe('snapshot API', () => {
let fetchMock: jest.SpyInstance>;
diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts
index 0354cfeac7b07e..4ec35d8cd6c6f1 100644
--- a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts
@@ -7,7 +7,7 @@
import { call, put } from 'redux-saga/effects';
import { fetchEffectFactory } from '../fetch_effect';
import { indexStatusAction } from '../../actions';
-import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error';
+import { HttpFetchError } from 'src/core/public';
import { StatesIndexStatus } from '../../../../common/runtime_types';
import { fetchIndexStatus } from '../../api';
diff --git a/x-pack/package.json b/x-pack/package.json
index 7d2dcb789c2ad0..2a8827a1ed75ba 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -185,7 +185,7 @@
"@elastic/ems-client": "7.8.0",
"@elastic/eui": "22.3.0",
"@elastic/filesaver": "1.1.2",
- "@elastic/maki": "6.2.0",
+ "@elastic/maki": "6.3.0",
"@elastic/node-crypto": "1.1.1",
"@elastic/numeral": "2.4.0",
"@kbn/babel-preset": "1.0.0",
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 82cc09f5e9eca6..d6c85606edc2c5 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -263,7 +263,7 @@ Kibana ships with a set of built-in action types:
| Type | Id | Description |
| ------------------------- | ------------- | ------------------------------------------------------------------ |
-| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` |
+| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger |
| [Email](#email) | `.email` | Sends an email using SMTP |
| [Slack](#slack) | `.slack` | Posts a message to a slack channel |
| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch |
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 955e1569380a5d..14441bfd52dd73 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -348,9 +348,6 @@ describe('get()', () => {
actionTypeId: '.slack',
isPreconfigured: true,
name: 'test',
- config: {
- foo: 'bar',
- },
});
expect(savedObjectsClient.get).not.toHaveBeenCalled();
});
@@ -418,9 +415,6 @@ describe('getAll()', () => {
actionTypeId: '.slack',
isPreconfigured: true,
name: 'test',
- config: {
- foo: 'bar',
- },
referencedByCount: 2,
},
]);
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 8f73bfb31ea4de..f52e293296955c 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -152,7 +152,6 @@ export class ActionsClient {
id,
actionTypeId: preconfiguredActionsList.actionTypeId,
name: preconfiguredActionsList.name,
- config: preconfiguredActionsList.config,
isPreconfigured: true,
};
}
@@ -184,7 +183,6 @@ export class ActionsClient {
id: preconfiguredAction.id,
actionTypeId: preconfiguredAction.actionTypeId,
name: preconfiguredAction.name,
- config: preconfiguredAction.config,
isPreconfigured: true,
})),
].sort((a, b) => a.name.localeCompare(b.name));
diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts
index 22cf0dd7f8ace6..a01c5e9c2452c6 100644
--- a/x-pack/plugins/actions/server/routes/create.test.ts
+++ b/x-pack/plugins/actions/server/routes/create.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createActionRoute } from './create';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess, ActionTypeDisabledError } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -20,7 +20,7 @@ beforeEach(() => {
describe('createActionRoute', () => {
it('creates an action with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
createActionRoute(router, licenseState);
@@ -83,7 +83,7 @@ describe('createActionRoute', () => {
it('ensures the license allows creating actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
createActionRoute(router, licenseState);
@@ -107,7 +107,7 @@ describe('createActionRoute', () => {
it('ensures the license check prevents creating actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
@@ -135,7 +135,7 @@ describe('createActionRoute', () => {
it('ensures the action type gets validated for the license', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
createActionRoute(router, licenseState);
diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts
index 6fb526628cb021..b0929f48d7875e 100644
--- a/x-pack/plugins/actions/server/routes/delete.test.ts
+++ b/x-pack/plugins/actions/server/routes/delete.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { deleteActionRoute } from './delete';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -20,7 +20,7 @@ beforeEach(() => {
describe('deleteActionRoute', () => {
it('deletes an action with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
deleteActionRoute(router, licenseState);
@@ -65,7 +65,7 @@ describe('deleteActionRoute', () => {
it('ensures the license allows deleting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
deleteActionRoute(router, licenseState);
@@ -86,7 +86,7 @@ describe('deleteActionRoute', () => {
it('ensures the license check prevents deleting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts
index 3a3ed1257f5764..1cd6343a39dcff 100644
--- a/x-pack/plugins/actions/server/routes/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/execute.test.ts
@@ -5,7 +5,7 @@
*/
import { executeActionRoute } from './execute';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('executeActionRoute', () => {
it('executes an action with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const [context, req, res] = mockHandlerArguments(
{},
@@ -77,7 +77,7 @@ describe('executeActionRoute', () => {
it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const [context, req, res] = mockHandlerArguments(
{},
@@ -115,7 +115,7 @@ describe('executeActionRoute', () => {
it('ensures the license allows action execution', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const [context, req, res] = mockHandlerArguments(
{},
@@ -147,7 +147,7 @@ describe('executeActionRoute', () => {
it('ensures the license check prevents action execution', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
@@ -183,7 +183,7 @@ describe('executeActionRoute', () => {
it('ensures the action type gets validated for the license', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const [context, req, res] = mockHandlerArguments(
{},
diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts
index f4e834a5b767ca..7de4d93d91bb63 100644
--- a/x-pack/plugins/actions/server/routes/get.test.ts
+++ b/x-pack/plugins/actions/server/routes/get.test.ts
@@ -5,7 +5,7 @@
*/
import { getActionRoute } from './get';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('getActionRoute', () => {
it('gets an action with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getActionRoute(router, licenseState);
@@ -75,7 +75,7 @@ describe('getActionRoute', () => {
it('ensures the license allows getting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getActionRoute(router, licenseState);
@@ -105,7 +105,7 @@ describe('getActionRoute', () => {
it('ensures the license check prevents getting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts
index 6499427b8c1a53..1422bf09657869 100644
--- a/x-pack/plugins/actions/server/routes/get_all.test.ts
+++ b/x-pack/plugins/actions/server/routes/get_all.test.ts
@@ -5,7 +5,7 @@
*/
import { getAllActionRoute } from './get_all';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('getAllActionRoute', () => {
it('get all actions with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAllActionRoute(router, licenseState);
@@ -57,7 +57,7 @@ describe('getAllActionRoute', () => {
it('ensures the license allows getting all actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAllActionRoute(router, licenseState);
@@ -85,7 +85,7 @@ describe('getAllActionRoute', () => {
it('ensures the license check prevents getting all actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts
index 76fb636a75be7f..38410f45f091da 100644
--- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts
+++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts
@@ -5,7 +5,7 @@
*/
import { listActionTypesRoute } from './list_action_types';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('listActionTypesRoute', () => {
it('lists action types with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
listActionTypesRoute(router, licenseState);
@@ -67,7 +67,7 @@ describe('listActionTypesRoute', () => {
it('ensures the license allows listing action types', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
listActionTypesRoute(router, licenseState);
@@ -105,7 +105,7 @@ describe('listActionTypesRoute', () => {
it('ensures the license check prevents listing action types', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts
index 161fb4398af1de..0c2e9b622ee57e 100644
--- a/x-pack/plugins/actions/server/routes/update.test.ts
+++ b/x-pack/plugins/actions/server/routes/update.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { updateActionRoute } from './update';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess, ActionTypeDisabledError } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -20,7 +20,7 @@ beforeEach(() => {
describe('updateActionRoute', () => {
it('updates an action with proper parameters', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateActionRoute(router, licenseState);
@@ -86,7 +86,7 @@ describe('updateActionRoute', () => {
it('ensures the license allows deleting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateActionRoute(router, licenseState);
@@ -125,7 +125,7 @@ describe('updateActionRoute', () => {
it('ensures the license check prevents deleting actions', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
@@ -168,7 +168,7 @@ describe('updateActionRoute', () => {
it('ensures the action type gets validated for the license', async () => {
const licenseState = licenseStateMock.create();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateActionRoute(router, licenseState);
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index 92e38d77314f83..088398b40830e4 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -54,7 +54,7 @@ export interface ActionResult {
id: string;
actionTypeId: string;
name: string;
- config: Record;
+ config?: Record;
isPreconfigured: boolean;
}
diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts
index c6a0da2bd91916..294dd7e002d715 100644
--- a/x-pack/plugins/alerting/server/routes/create.test.ts
+++ b/x-pack/plugins/alerting/server/routes/create.test.ts
@@ -5,7 +5,7 @@
*/
import { createAlertRoute } from './create';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -68,7 +68,7 @@ describe('createAlertRoute', () => {
it('creates an alert with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
createAlertRoute(router, licenseState);
@@ -134,7 +134,7 @@ describe('createAlertRoute', () => {
it('ensures the license allows creating alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
createAlertRoute(router, licenseState);
@@ -151,7 +151,7 @@ describe('createAlertRoute', () => {
it('ensures the license check prevents creating alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts
index 36bb485817c156..d6afdbd85e77af 100644
--- a/x-pack/plugins/alerting/server/routes/delete.test.ts
+++ b/x-pack/plugins/alerting/server/routes/delete.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { deleteAlertRoute } from './delete';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -23,7 +23,7 @@ beforeEach(() => {
describe('deleteAlertRoute', () => {
it('deletes an alert with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
deleteAlertRoute(router, licenseState);
@@ -66,7 +66,7 @@ describe('deleteAlertRoute', () => {
it('ensures the license allows deleting alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
deleteAlertRoute(router, licenseState);
@@ -85,7 +85,7 @@ describe('deleteAlertRoute', () => {
it('ensures the license check prevents deleting alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts
index 622b562ec69116..fde095e9145b66 100644
--- a/x-pack/plugins/alerting/server/routes/disable.test.ts
+++ b/x-pack/plugins/alerting/server/routes/disable.test.ts
@@ -5,7 +5,7 @@
*/
import { disableAlertRoute } from './disable';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -23,7 +23,7 @@ beforeEach(() => {
describe('disableAlertRoute', () => {
it('disables an alert', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
disableAlertRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts
index 5a7b027e8ef546..e4e89e3f06380b 100644
--- a/x-pack/plugins/alerting/server/routes/enable.test.ts
+++ b/x-pack/plugins/alerting/server/routes/enable.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { enableAlertRoute } from './enable';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('enableAlertRoute', () => {
it('enables an alert', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
enableAlertRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts
index 391d6df3f99317..879d9498cda03f 100644
--- a/x-pack/plugins/alerting/server/routes/find.test.ts
+++ b/x-pack/plugins/alerting/server/routes/find.test.ts
@@ -5,7 +5,7 @@
*/
import { findAlertRoute } from './find';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -24,7 +24,7 @@ beforeEach(() => {
describe('findAlertRoute', () => {
it('finds alerts with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
findAlertRoute(router, licenseState);
@@ -95,7 +95,7 @@ describe('findAlertRoute', () => {
it('ensures the license allows finding alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
findAlertRoute(router, licenseState);
@@ -123,7 +123,7 @@ describe('findAlertRoute', () => {
it('ensures the license check prevents finding alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts
index 4506700c6d9cc1..fe89c86edc2b17 100644
--- a/x-pack/plugins/alerting/server/routes/get.test.ts
+++ b/x-pack/plugins/alerting/server/routes/get.test.ts
@@ -5,7 +5,7 @@
*/
import { getAlertRoute } from './get';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -55,7 +55,7 @@ describe('getAlertRoute', () => {
it('gets an alert with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAlertRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
@@ -90,7 +90,7 @@ describe('getAlertRoute', () => {
it('ensures the license allows getting alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAlertRoute(router, licenseState);
@@ -113,7 +113,7 @@ describe('getAlertRoute', () => {
it('ensures the license check prevents getting alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts
index eb51c96b88e5ea..20a420ca00986f 100644
--- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts
+++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts
@@ -5,10 +5,10 @@
*/
import { getAlertStateRoute } from './get_alert_state';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
-import { SavedObjectsErrorHelpers } from 'src/core/server/saved_objects';
+import { SavedObjectsErrorHelpers } from 'src/core/server';
import { alertsClientMock } from '../alerts_client.mock';
const alertsClient = alertsClientMock.create();
@@ -41,7 +41,7 @@ describe('getAlertStateRoute', () => {
it('gets alert state', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAlertStateRoute(router, licenseState);
@@ -84,7 +84,7 @@ describe('getAlertStateRoute', () => {
it('returns NO-CONTENT when alert exists but has no task state yet', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAlertStateRoute(router, licenseState);
@@ -127,7 +127,7 @@ describe('getAlertStateRoute', () => {
it('returns NOT-FOUND when alert is not found', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
getAlertStateRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts
index 42c83a7c04deba..7c50fbf561e590 100644
--- a/x-pack/plugins/alerting/server/routes/health.test.ts
+++ b/x-pack/plugins/alerting/server/routes/health.test.ts
@@ -5,7 +5,7 @@
*/
import { healthRoute } from './health';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { verifyApiAccess } from '../lib/license_api_access';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('healthRoute', () => {
it('registers the route', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -35,7 +35,7 @@ describe('healthRoute', () => {
});
it('queries the usage api', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -64,7 +64,7 @@ describe('healthRoute', () => {
});
it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -88,7 +88,7 @@ describe('healthRoute', () => {
});
it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -112,7 +112,7 @@ describe('healthRoute', () => {
});
it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -136,7 +136,7 @@ describe('healthRoute', () => {
});
it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -162,7 +162,7 @@ describe('healthRoute', () => {
});
it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
@@ -188,7 +188,7 @@ describe('healthRoute', () => {
});
it('evaluates security and tls enabled to mean that the user can generate keys', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
const licenseState = mockLicenseState();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts
index 723fd86fca8b5e..cc83d10fb97fdb 100644
--- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts
+++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts
@@ -5,7 +5,7 @@
*/
import { listAlertTypesRoute } from './list_alert_types';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('listAlertTypesRoute', () => {
it('lists alert types with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
listAlertTypesRoute(router, licenseState);
@@ -79,7 +79,7 @@ describe('listAlertTypesRoute', () => {
it('ensures the license allows listing alert types', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
listAlertTypesRoute(router, licenseState);
@@ -117,7 +117,7 @@ describe('listAlertTypesRoute', () => {
it('ensures the license check prevents listing alert types', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts
index 4c880e176d2df6..5ef9e3694f8f45 100644
--- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts
+++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts
@@ -5,7 +5,7 @@
*/
import { muteAllAlertRoute } from './mute_all';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('muteAllAlertRoute', () => {
it('mute an alert', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
muteAllAlertRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts
index 939864972c35df..2e6adedb76df9b 100644
--- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts
+++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts
@@ -5,7 +5,7 @@
*/
import { muteAlertInstanceRoute } from './mute_instance';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('muteAlertInstanceRoute', () => {
it('mutes an alert instance', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
muteAlertInstanceRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts
index cd14e9b2e71726..1756dbd3fb41d0 100644
--- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts
+++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { unmuteAllAlertRoute } from './unmute_all';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -21,7 +21,7 @@ beforeEach(() => {
describe('unmuteAllAlertRoute', () => {
it('unmutes an alert', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
unmuteAllAlertRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts
index d74934f6917103..9b9542c606741c 100644
--- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts
+++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts
@@ -5,7 +5,7 @@
*/
import { unmuteAlertInstanceRoute } from './unmute_instance';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('unmuteAlertInstanceRoute', () => {
it('unmutes an alert instance', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
unmuteAlertInstanceRoute(router, licenseState);
diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts
index c3628617f861fe..cd96f289b87147 100644
--- a/x-pack/plugins/alerting/server/routes/update.test.ts
+++ b/x-pack/plugins/alerting/server/routes/update.test.ts
@@ -5,7 +5,7 @@
*/
import { updateAlertRoute } from './update';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
@@ -45,7 +45,7 @@ describe('updateAlertRoute', () => {
it('updates an alert with proper parameters', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateAlertRoute(router, licenseState);
@@ -128,7 +128,7 @@ describe('updateAlertRoute', () => {
it('ensures the license allows updating alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateAlertRoute(router, licenseState);
@@ -171,7 +171,7 @@ describe('updateAlertRoute', () => {
it('ensures the license check prevents updating alerts', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts
index 5e9821ac005e2b..0347feb24a2359 100644
--- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts
+++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts
@@ -5,7 +5,7 @@
*/
import { updateApiKeyRoute } from './update_api_key';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { alertsClientMock } from '../alerts_client.mock';
@@ -22,7 +22,7 @@ beforeEach(() => {
describe('updateApiKeyRoute', () => {
it('updates api key for an alert', async () => {
const licenseState = mockLicenseState();
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
updateApiKeyRoute(router, licenseState);
diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
index 8cfd736a336c2d..6268f5899d7ffd 100644
--- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
+++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
@@ -143,11 +143,15 @@ export const agentConfigurationSearchRoute = createRoute(core => ({
params: {
body: t.intersection([
t.type({ service: serviceRt }),
- t.partial({ etag: t.string })
+ t.partial({ etag: t.string, mark_as_applied_by_agent: t.boolean })
])
},
handler: async ({ context, request }) => {
- const { service, etag } = context.params.body;
+ const {
+ service,
+ etag,
+ mark_as_applied_by_agent: markAsAppliedByAgent
+ } = context.params.body;
const setup = await setupRequest(context, request);
const config = await searchConfigurations({
@@ -166,9 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({
`Config was found for ${service.name}/${service.environment}`
);
- // update `applied_by_agent` field if etags match
+ // update `applied_by_agent` field
+ // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags)
+ // or if etags match.
// this happens in the background and doesn't block the response
- if (etag === config._source.etag && !config._source.applied_by_agent) {
+ if (
+ (markAsAppliedByAgent || etag === config._source.etag) &&
+ !config._source.applied_by_agent
+ ) {
markAppliedByAgent({ id: config._id, body: config._source, setup });
}
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts
index e98b2f52089b32..f6c4fce76f134c 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts
@@ -52,5 +52,5 @@ export interface SpanRaw extends APMBaseDoc {
id: string;
};
observer?: Observer;
- child_ids?: string[];
+ child?: { id: string[] };
}
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
index 47f7d503e32b8d..29df97c5f84763 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
@@ -39,7 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
if (myCaseConfigure.saved_objects.length === 0) {
throw Boom.conflict(
- 'You can not patch this configuration since you did not created first with a post'
+ 'You can not patch this configuration since you did not created first with a post.'
);
}
diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts
index 4d77c5c68c69fc..b087405c7bc5b3 100644
--- a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts
+++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IndexPatternRetriever } from './index_pattern';
-import { AgentService } from '../../ingest_manager/common/types';
+import { AgentService } from '../../ingest_manager/server';
/**
* A singleton that holds shared services that are initialized during the start up phase
diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts
index 05cfd91f2777a9..dcedd27fc5a3d4 100644
--- a/x-pack/plugins/endpoint/server/index_pattern.ts
+++ b/x-pack/plugins/endpoint/server/index_pattern.ts
@@ -5,7 +5,7 @@
*/
import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server';
import { EndpointAppConstants } from '../common/types';
-import { ESIndexPatternService } from '../../ingest_manager/common/types';
+import { ESIndexPatternService } from '../../ingest_manager/server';
export interface IndexPatternRetriever {
getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise;
diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts
index 8d677cf37f0665..519ca15cf84279 100644
--- a/x-pack/plugins/endpoint/server/mocks.ts
+++ b/x-pack/plugins/endpoint/server/mocks.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { AgentService, IngestManagerStartupContract } from '../../ingest_manager/common/types';
+import { AgentService, IngestManagerStartContract } from '../../ingest_manager/server';
/**
* Creates a mock IndexPatternRetriever for use in tests.
@@ -46,9 +46,9 @@ export const createMockAgentService = (): jest.Mocked => {
* @param indexPattern a string index pattern to return when called by a test
* @returns the same value as `indexPattern` parameter
*/
-export const createMockIngestManagerStartupContract = (
+export const createMockIngestManagerStartContract = (
indexPattern: string
-): IngestManagerStartupContract => {
+): IngestManagerStartContract => {
return {
esIndexPatternService: {
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts
index 44b9c3eabbd6d8..45e9591a149750 100644
--- a/x-pack/plugins/endpoint/server/plugin.test.ts
+++ b/x-pack/plugins/endpoint/server/plugin.test.ts
@@ -11,7 +11,7 @@ import {
} from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
-import { createMockIngestManagerStartupContract } from './mocks';
+import { createMockIngestManagerStartContract } from './mocks';
describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
@@ -51,7 +51,7 @@ describe('test endpoint plugin', () => {
it('test properly start plugin', async () => {
mockedEndpointPluginStartDependencies = {
- ingestManager: createMockIngestManagerStartupContract(''),
+ ingestManager: createMockIngestManagerStartContract(''),
};
await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies);
expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy();
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index f7ff32bdbfcbfc..f3cc569ad16a7f 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -14,13 +14,13 @@ import { registerResolverRoutes } from './routes/resolver';
import { registerIndexPatternRoute } from './routes/index_pattern';
import { registerEndpointRoutes } from './routes/metadata';
import { IngestIndexPatternRetriever } from './index_pattern';
-import { IngestManagerStartupContract } from '../../ingest_manager/common/types';
+import { IngestManagerStartContract } from '../../ingest_manager/server';
import { EndpointAppContextService } from './endpoint_app_context_services';
export type EndpointPluginStart = void;
export type EndpointPluginSetup = void;
export interface EndpointPluginStartDependencies {
- ingestManager: IngestManagerStartupContract;
+ ingestManager: IngestManagerStartContract;
}
export interface EndpointPluginSetupDependencies {
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
index 84930118df2385..8f0c0b4c2efaf1 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
@@ -26,7 +26,7 @@ import { registerEndpointRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import * as data from '../../test_data/all_metadata_data.json';
import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks';
-import { AgentService } from '../../../../ingest_manager/common/types';
+import { AgentService } from '../../../../ingest_manager/server';
import Boom from 'boom';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts
index 268421235b4b2d..7b3d01f3baa89c 100644
--- a/x-pack/plugins/event_log/server/es/names.mock.ts
+++ b/x-pack/plugins/event_log/server/es/names.mock.ts
@@ -10,7 +10,7 @@ const createNamesMock = () => {
const mock: jest.Mocked = {
base: '.kibana',
alias: '.kibana-event-log-8.0.0',
- ilmPolicy: '.kibana-event-log-policy',
+ ilmPolicy: 'kibana-event-log-policy',
indexPattern: '.kibana-event-log-*',
indexPatternWithVersion: '.kibana-event-log-8.0.0-*',
initialIndex: '.kibana-event-log-8.0.0-000001',
diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts
index baefd756bb1ed0..bc6a4c9a52fac4 100644
--- a/x-pack/plugins/event_log/server/es/names.test.ts
+++ b/x-pack/plugins/event_log/server/es/names.test.ts
@@ -23,4 +23,10 @@ describe('getEsNames()', () => {
expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`);
expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`);
});
+
+ test('ilm policy name does not contain dot prefix', () => {
+ const base = '.XYZ';
+ const esNames = getEsNames(base);
+ expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy');
+ });
});
diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts
index d55d02a16fc9a7..8cd56a89d3fbec 100644
--- a/x-pack/plugins/event_log/server/es/names.ts
+++ b/x-pack/plugins/event_log/server/es/names.ts
@@ -22,10 +22,13 @@ export interface EsNames {
export function getEsNames(baseName: string): EsNames {
const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`;
const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`;
+ const eventLogPolicyName = `${
+ baseName.startsWith('.') ? baseName.substring(1) : baseName
+ }${EVENT_LOG_NAME_SUFFIX}-policy`;
return {
base: baseName,
alias: eventLogNameWithVersion,
- ilmPolicy: `${eventLogName}-policy`,
+ ilmPolicy: `${eventLogPolicyName}`,
indexPattern: `${eventLogName}-*`,
indexPatternWithVersion: `${eventLogNameWithVersion}-*`,
initialIndex: `${eventLogNameWithVersion}-000001`,
diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts
index 3b250b74620097..43883ea4e384ce 100644
--- a/x-pack/plugins/event_log/server/event_log_service.test.ts
+++ b/x-pack/plugins/event_log/server/event_log_service.test.ts
@@ -7,7 +7,7 @@
import { IEventLogConfig } from './types';
import { EventLogService } from './event_log_service';
import { contextMock } from './es/context.mock';
-import { loggingServiceMock } from '../../../../src/core/server/logging/logging_service.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
const loggingService = loggingServiceMock.create();
const systemLogger = loggingService.get();
diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts
index a8d75bc6c2e5a2..6db16ebadd4ce5 100644
--- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts
+++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts
@@ -7,8 +7,7 @@
import { EventLogClientService } from './event_log_start_service';
import { contextMock } from './es/context.mock';
import { KibanaRequest } from 'kibana/server';
-import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock';
-import { savedObjectsClientMock } from 'src/core/server/mocks';
+import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks';
jest.mock('./event_log_client');
diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts
index 673bac4f396e1b..6a745931420c0c 100644
--- a/x-pack/plugins/event_log/server/event_logger.test.ts
+++ b/x-pack/plugins/event_log/server/event_logger.test.ts
@@ -9,20 +9,20 @@ import { ECS_VERSION } from './types';
import { EventLogService } from './event_log_service';
import { EsContext } from './es/context';
import { contextMock } from './es/context.mock';
-import { loggerMock, MockedLogger } from '../../../../src/core/server/logging/logger.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
import { delay } from './lib/delay';
import { EVENT_LOGGED_PREFIX } from './event_logger';
const KIBANA_SERVER_UUID = '424-24-2424';
describe('EventLogger', () => {
- let systemLogger: MockedLogger;
+ let systemLogger: ReturnType;
let esContext: EsContext;
let service: IEventLogService;
let eventLogger: IEventLogger;
beforeEach(() => {
- systemLogger = loggerMock.create();
+ systemLogger = loggingServiceMock.createLogger();
esContext = contextMock.create();
service = new EventLogService({
esContext,
@@ -153,7 +153,10 @@ describe('EventLogger', () => {
});
// return the next logged event; throw if not an event
-async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1): Promise {
+async function waitForLogEvent(
+ mockLogger: ReturnType,
+ waitSeconds: number = 1
+): Promise {
const result = await waitForLog(mockLogger, waitSeconds);
if (typeof result === 'string') throw new Error('expecting an event');
return result;
@@ -161,7 +164,7 @@ async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1
// return the next logged message; throw if it is an event
async function waitForLogMessage(
- mockLogger: MockedLogger,
+ mockLogger: ReturnType,
waitSeconds: number = 1
): Promise {
const result = await waitForLog(mockLogger, waitSeconds);
@@ -171,7 +174,7 @@ async function waitForLogMessage(
// return the next logged message, if it's an event log entry, parse it
async function waitForLog(
- mockLogger: MockedLogger,
+ mockLogger: ReturnType,
waitSeconds: number = 1
): Promise {
const intervals = 4;
diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
index c1bb6d70879f3b..dd6d15a6e48431 100644
--- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
+++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
@@ -5,7 +5,7 @@
*/
import { createBoundedQueue } from './bounded_queue';
-import { loggingServiceMock } from '../../../../../src/core/server/logging/logging_service.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
const loggingService = loggingServiceMock.create();
const logger = loggingService.get();
diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts
index 844a84dc117a9c..f47df499d742f6 100644
--- a/x-pack/plugins/event_log/server/routes/find.test.ts
+++ b/x-pack/plugins/event_log/server/routes/find.test.ts
@@ -5,7 +5,7 @@
*/
import { findRoute } from './find';
-import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
+import { httpServiceMock } from 'src/core/server/mocks';
import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments';
import { eventLogClientMock } from '../event_log_client.mock';
@@ -17,7 +17,7 @@ beforeEach(() => {
describe('find', () => {
it('finds events with proper parameters', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
findRoute(router);
@@ -56,7 +56,7 @@ describe('find', () => {
});
it('supports optional pagination parameters', async () => {
- const router: RouterMock = mockRouter.create();
+ const router = httpServiceMock.createRouter();
findRoute(router);
diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts
new file mode 100644
index 00000000000000..65cde03ec98e78
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiPanel } from '@elastic/eui';
+import { euiStyled } from '../../../observability/public';
+
+export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({
+ grow: false,
+ paddingSize: 'none',
+}))`
+ border-top: none;
+ border-right: none;
+ border-left: none;
+ border-radius: 0;
+ padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`};
+`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
new file mode 100644
index 00000000000000..f0bc404dc37970
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
+import React, { ReactNode } from 'react';
+import { withTheme, EuiTheme } from '../../../../../../observability/public';
+
+interface Props {
+ label: string;
+ onClick: () => void;
+ theme: EuiTheme;
+ children: ReactNode;
+}
+
+export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => {
+ return (
+
+
+ {label}
+
+
+
+ {children}
+
+
+
+ );
+});
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
similarity index 57%
rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx
rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
index d6a87a0197f5f7..708d5f7d759071 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx
@@ -7,17 +7,13 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
-import { Toolbar } from '../../../components/eui/toolbar';
-import { WaffleTimeControls } from './components/waffle/waffle_time_controls';
-import { WaffleInventorySwitcher } from './components/waffle/waffle_inventory_switcher';
-import { SearchBar } from './components/search_bar';
+import { WaffleTimeControls } from './waffle/waffle_time_controls';
+import { SearchBar } from './search_bar';
+import { ToolbarPanel } from '../../../../components/toolbar_panel';
-export const SnapshotToolbar = () => (
-
+export const FilterBar = () => (
+
-
-
-
@@ -25,5 +21,5 @@ export const SnapshotToolbar = () => (
-
+
);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
index bc8be9862fe636..a71e43874b4801 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
@@ -4,20 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { useInterval } from 'react-use';
-import { euiPaletteColorBlind } from '@elastic/eui';
-import { NodesOverview } from './nodes_overview';
-import { Toolbar } from './toolbars/toolbar';
+import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
+import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview';
import { PageContent } from '../../../../components/page';
import { useSnapshot } from '../hooks/use_snaphot';
-import { useInventoryMeta } from '../hooks/use_inventory_meta';
import { useWaffleTimeContext } from '../hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../hooks/use_waffle_filters';
import { useWaffleOptionsContext } from '../hooks/use_waffle_options';
import { useSourceContext } from '../../../../containers/source';
import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib';
+import { euiStyled } from '../../../../../../observability/public';
+import { Toolbar } from './toolbars/toolbar';
+import { ViewSwitcher } from './waffle/view_switcher';
+import { SavedViews } from './saved_views';
+import { IntervalLabel } from './waffle/interval_label';
+import { Legend } from './waffle/legend';
+import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
const euiVisColorPalette = euiPaletteColorBlind();
@@ -34,7 +40,6 @@ export const Layout = () => {
autoBounds,
boundsOverride,
} = useWaffleOptionsContext();
- const { accounts, regions } = useInventoryMeta(sourceId, nodeType);
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
const { loading, nodes, reload, interval } = useSnapshot(
@@ -72,25 +77,75 @@ export const Layout = () => {
isAutoReloading ? 5000 : null
);
+ const intervalAsString = convertIntervalToString(interval);
+ const dataBounds = calculateBoundsFromNodes(nodes);
+ const bounds = autoBounds ? dataBounds : boundsOverride;
+ const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
+
return (
<>
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
);
};
+
+const MainContainer = euiStyled.div`
+ position: relative;
+ flex: 1 1 auto;
+`;
+
+const TopActionContainer = euiStyled.div`
+ padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`};
+`;
+
+const BottomActionContainer = euiStyled.div`
+ background-color: ${props => props.theme.eui.euiPageBackgroundColor};
+ padding: ${props => props.theme.eui.paddingSizes.m} ${props =>
+ props.theme.eui.paddingSizes.m} ${props => props.theme.eui.paddingSizes.s};
+ position: absolute;
+ left: 0;
+ bottom: 4px;
+ right: 0;
+`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
index afbfd2a079253d..966a327f40bc16 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
@@ -4,31 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { get, max, min } from 'lodash';
-import React from 'react';
+import { max, min } from 'lodash';
+import React, { useCallback } from 'react';
+import { InventoryItemType } from '../../../../../common/inventory_models/types';
import { euiStyled } from '../../../../../../observability/public';
-import {
- InfraFormatterType,
- InfraWaffleMapBounds,
- InfraWaffleMapOptions,
-} from '../../../../lib/lib';
-import { createFormatter } from '../../../../utils/formatters';
+import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib';
import { NoData } from '../../../../components/empty_states';
import { InfraLoadingPanel } from '../../../../components/loading';
import { Map } from './waffle/map';
-import { ViewSwitcher } from './waffle/view_switcher';
import { TableView } from './table_view';
-import {
- SnapshotNode,
- SnapshotCustomMetricInputRT,
-} from '../../../../../common/http_api/snapshot_api';
-import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
-import { InventoryItemType } from '../../../../../common/inventory_models/types';
-import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric';
+import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
export interface KueryFilterQuery {
kind: 'kuery';
@@ -43,74 +30,13 @@ interface Props {
reload: () => void;
onDrilldown: (filter: KueryFilterQuery) => void;
currentTime: number;
- onViewChange: (view: string) => void;
view: string;
boundsOverride: InfraWaffleMapBounds;
autoBounds: boolean;
- interval: string;
-}
-
-interface MetricFormatter {
- formatter: InfraFormatterType;
- template: string;
- bounds?: { min: number; max: number };
-}
-
-interface MetricFormatters {
- [key: string]: MetricFormatter;
+ formatter: InfraFormatter;
}
-const METRIC_FORMATTERS: MetricFormatters = {
- ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' },
- ['cpu']: {
- formatter: InfraFormatterType.percent,
- template: '{{value}}',
- },
- ['memory']: {
- formatter: InfraFormatterType.percent,
- template: '{{value}}',
- },
- ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
- ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
- ['logRate']: {
- formatter: InfraFormatterType.abbreviatedNumber,
- template: '{{value}}/s',
- },
- ['diskIOReadBytes']: {
- formatter: InfraFormatterType.bytes,
- template: '{{value}}/s',
- },
- ['diskIOWriteBytes']: {
- formatter: InfraFormatterType.bytes,
- template: '{{value}}/s',
- },
- ['s3BucketSize']: {
- formatter: InfraFormatterType.bytes,
- template: '{{value}}',
- },
- ['s3TotalRequests']: {
- formatter: InfraFormatterType.abbreviatedNumber,
- template: '{{value}}',
- },
- ['s3NumberOfObjects']: {
- formatter: InfraFormatterType.abbreviatedNumber,
- template: '{{value}}',
- },
- ['s3UploadBytes']: {
- formatter: InfraFormatterType.bytes,
- template: '{{value}}',
- },
- ['s3DownloadBytes']: {
- formatter: InfraFormatterType.bytes,
- template: '{{value}}',
- },
- ['sqsOldestMessage']: {
- formatter: InfraFormatterType.number,
- template: '{{value}} seconds',
- },
-};
-
-const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
+export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
const maxValues = nodes.map(node => node.metric.max);
const minValues = nodes.map(node => node.metric.value);
// if there is only one value then we need to set the bottom range to zero for min
@@ -122,141 +48,97 @@ const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds =
return { min: min(minValues) || 0, max: max(maxValues) || 0 };
};
-export const NodesOverview = class extends React.Component {
- public static displayName = 'Waffle';
- public render() {
- const {
- autoBounds,
- boundsOverride,
- loading,
- nodes,
- nodeType,
- reload,
- view,
- currentTime,
- options,
- interval,
- } = this.props;
- if (loading) {
- return (
-
- );
- } else if (!loading && nodes && nodes.length === 0) {
- return (
- {
- reload();
- }}
- testString="noMetricsDataPrompt"
- />
- );
- }
- const dataBounds = calculateBoundsFromNodes(nodes);
- const bounds = autoBounds ? dataBounds : boundsOverride;
- const intervalAsString = convertIntervalToString(interval);
+export const NodesOverview = ({
+ autoBounds,
+ boundsOverride,
+ loading,
+ nodes,
+ nodeType,
+ reload,
+ view,
+ currentTime,
+ options,
+ formatter,
+ onDrilldown,
+}: Props) => {
+ const handleDrilldown = useCallback(
+ (filter: string) => {
+ onDrilldown({
+ kind: 'kuery',
+ expression: filter,
+ });
+ return;
+ },
+ [onDrilldown]
+ );
+
+ const noData = !loading && nodes && nodes.length === 0;
+ if (loading) {
+ return (
+
+ );
+ } else if (noData) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {view === 'table' ? (
-
-
-
- ) : (
-
-
-
- )}
-
+ {
+ reload();
+ }}
+ testString="noMetricsDataPrompt"
+ />
);
}
+ const dataBounds = calculateBoundsFromNodes(nodes);
+ const bounds = autoBounds ? dataBounds : boundsOverride;
- private handleViewChange = (view: string) => this.props.onViewChange(view);
-
- // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
- private formatter = (val: string | number) => {
- const { metric } = this.props.options;
- if (SnapshotCustomMetricInputRT.is(metric)) {
- const formatter = createFormatterForMetric(metric);
- return formatter(val);
- }
- const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count);
- if (val == null) {
- return '';
- }
- const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
- return formatter(val);
- };
-
- private handleDrilldown = (filter: string) => {
- this.props.onDrilldown({
- kind: 'kuery',
- expression: filter,
- });
- return;
- };
+ if (view === 'table') {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
};
-const MainContainer = euiStyled.div`
- position: relative;
- flex: 1 1 auto;
-`;
-
const TableContainer = euiStyled.div`
padding: ${props => props.theme.eui.paddingSizes.l};
`;
-const ViewSwitcherContainer = euiStyled.div`
- padding: ${props => props.theme.eui.paddingSizes.l};
-`;
-
const MapContainer = euiStyled.div`
position: absolute;
display: flex;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx
similarity index 68%
rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx
rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx
index eb40ea595662a2..356f0598e00d26 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
-import { SavedViewsToolbarControls } from '../../../../../components/saved_views/toolbar_control';
-import { inventoryViewSavedObjectType } from '../../../../../../common/saved_objects/inventory_view';
-import { useWaffleViewState } from '../../hooks/use_waffle_view_state';
+import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
+import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view';
+import { useWaffleViewState } from '../hooks/use_waffle_view_state';
export const SavedViews = () => {
const { viewState, defaultViewState, onViewChange } = useWaffleViewState();
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx
index 3ac9c2c1896286..e8485fb812586a 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx
@@ -6,6 +6,7 @@
import React, { FunctionComponent } from 'react';
import { EuiFlexItem } from '@elastic/eui';
+import { useSourceContext } from '../../../../../containers/source';
import {
SnapshotMetricInput,
SnapshotGroupBy,
@@ -19,7 +20,7 @@ import { InfraGroupByOptions } from '../../../../../lib/lib';
import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { WaffleOptionsState } from '../../hooks/use_waffle_options';
-import { SavedViews } from './save_views';
+import { useInventoryMeta } from '../../hooks/use_inventory_meta';
export interface ToolbarProps
extends Omit {
@@ -45,9 +46,6 @@ const wrapToolbarItems = (
<>
-
-
-
>
)}
@@ -56,10 +54,11 @@ const wrapToolbarItems = (
interface Props {
nodeType: InventoryItemType;
- regions: string[];
- accounts: InventoryCloudAccount[];
}
-export const Toolbar = ({ nodeType, accounts, regions }: Props) => {
+
+export const Toolbar = ({ nodeType }: Props) => {
+ const { sourceId } = useSourceContext();
+ const { accounts, regions } = useInventoryMeta(sourceId, nodeType);
const ToolbarItems = findToolbar(nodeType);
return wrapToolbarItems(ToolbarItems, accounts, regions);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx
index 86cc0d8ee62e00..ea53122984161b 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx
@@ -5,14 +5,14 @@
*/
import React from 'react';
-import { EuiFlexGroup } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SnapshotMetricType } from '../../../../../../common/inventory_models/types';
-import { Toolbar } from '../../../../../components/eui/toolbar';
-import { ToolbarProps } from './toolbar';
import { fieldToName } from '../../lib/field_to_display_name';
import { useSourceContext } from '../../../../../containers/source';
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
+import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher';
+import { ToolbarProps } from './toolbar';
interface Props {
children: (props: Omit) => React.ReactElement;
@@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => {
} = useWaffleOptionsContext();
const { createDerivedIndexPattern } = useSourceContext();
return (
-
-
- {props.children({
- createDerivedIndexPattern,
- changeMetric,
- changeGroupBy,
- changeAccount,
- changeRegion,
- changeCustomOptions,
- customOptions,
- groupBy,
- metric,
- nodeType,
- region,
- accountId,
- customMetrics,
- changeCustomMetrics,
- })}
-
-
+ <>
+
+
+
+ {props.children({
+ createDerivedIndexPattern,
+ changeMetric,
+ changeGroupBy,
+ changeAccount,
+ changeRegion,
+ changeCustomOptions,
+ customOptions,
+ groupBy,
+ metric,
+ nodeType,
+ region,
+ accountId,
+ customMetrics,
+ changeCustomMetrics,
+ })}
+ >
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx
new file mode 100644
index 00000000000000..dbbfb0f49c0e9c
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+interface Props {
+ intervalAsString?: string;
+}
+
+export const IntervalLabel = ({ intervalAsString }: Props) => {
+ if (!intervalAsString) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
index ccb4cc71924f42..ac699f96a75a6b 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
@@ -53,7 +53,7 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter
const LegendContainer = euiStyled.div`
position: absolute;
- bottom: 10px;
+ bottom: 0px;
left: 10px;
right: 10px;
`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
index 6ec21ad2e1b49b..30447e52442411 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
@@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound
const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop
const buttonComponent = (
= ({
}
})}
-
);
}}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx
index 08d5b3e9e06706..f91e9a4034bc28 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui';
+import { EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback } from 'react';
import { IFieldType } from 'src/plugins/data/public';
import {
@@ -21,6 +20,7 @@ import { ModeSwitcher } from './mode_switcher';
import { MetricsEditMode } from './metrics_edit_mode';
import { CustomMetricMode } from './types';
import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types';
+import { DropdownButton } from '../../dropdown_button';
interface Props {
options: Array<{ text: string; value: string }>;
@@ -132,17 +132,13 @@ export const WaffleMetricControls = ({
}
const button = (
-
-
-
+
+ {currentLabel}
+
);
return (
-
+ <>
-
+ >
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
index 78a2cad9ca7ee5..76756637eb69e7 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
@@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => {
label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', {
defaultMessage: 'Table view',
}),
- iconType: 'editorUnorderedList',
+ iconType: 'visTable',
},
];
return (
@@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => {
defaultMessage: 'Switch between table and map view',
})}
options={buttons}
- color="primary"
+ color="text"
+ buttonSize="m"
idSelected={view}
onChange={onChange}
+ isIconOnly
/>
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx
index a8b0cf21bce85e..3e4ff1de8291d8 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx
@@ -4,17 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiContextMenuPanelDescriptor,
- EuiFilterButton,
- EuiFilterGroup,
- EuiPopover,
- EuiContextMenu,
-} from '@elastic/eui';
+import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api';
+import { DropdownButton } from '../dropdown_button';
interface Props {
accountId: string;
@@ -63,32 +57,26 @@ export const WaffleAccountsControls = (props: Props) => {
[options, accountId, changeAccount]
);
+ const button = (
+
+ {currentLabel
+ ? currentLabel.name
+ : i18n.translate('xpack.infra.waffle.accountAllTitle', {
+ defaultMessage: 'All',
+ })}
+
+ );
+
return (
-
-
-
-
- }
- anchorPosition="downLeft"
- panelPaddingSize="none"
- closePopover={closePopover}
- >
-
-
-
+
+
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx
index bc763d2cf93785..c1f406f31e85e8 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx
@@ -9,8 +9,6 @@ import {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
- EuiFilterButton,
- EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -22,6 +20,7 @@ import { CustomFieldPanel } from './custom_field_panel';
import { euiStyled } from '../../../../../../../observability/public';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api';
+import { DropdownButton } from '../dropdown_button';
interface Props {
options: Array<{ text: string; field: string; toolTipContent?: string }>;
@@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent o != null)
// In this map the `o && o.field` is totally unnecessary but Typescript is
// too stupid to realize that the filter above prevents the next map from being null
- .map(o => {o && o.text})
+ .map(o => (
+
+ {o && o.text}
+
+ ))
) : (
);
+
const button = (
-
-
+
{buttonBody}
-
+
);
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
index 23e06823f407f4..e534c97eda0900 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx
@@ -4,19 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiPopover,
- EuiContextMenu,
- EuiFilterButton,
- EuiFilterGroup,
- EuiContextMenuPanelDescriptor,
-} from '@elastic/eui';
+import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
import { findInventoryModel } from '../../../../../../common/inventory_models';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
+import { DropdownButton } from '../dropdown_button';
const getDisplayNameForType = (type: InventoryItemType) => {
const inventoryModel = findInventoryModel(type);
@@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => {
return getDisplayNameForType(nodeType);
}, [nodeType]);
+ const button = (
+
+ {selectedText}
+
+ );
+
return (
-
-
-
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx
index 671e44f42ef6a5..9d759424cdc939 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx
@@ -4,16 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiContextMenuPanelDescriptor,
- EuiFilterButton,
- EuiFilterGroup,
- EuiPopover,
- EuiContextMenu,
-} from '@elastic/eui';
+import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { DropdownButton } from '../dropdown_button';
interface Props {
region?: string;
@@ -62,32 +56,25 @@ export const WaffleRegionControls = (props: Props) => {
[changeRegion, options, region]
);
+ const button = (
+
+ {currentLabel ||
+ i18n.translate('xpack.infra.waffle.region', {
+ defaultMessage: 'All',
+ })}
+
+ );
+
return (
-
-
-
-
- }
- anchorPosition="downLeft"
- panelPaddingSize="none"
- closePopover={closePopover}
- >
-
-
-
+
+
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
index e473aea7a1f0bc..3a2c33d1c824c5 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx
@@ -8,7 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
-import { SnapshotToolbar } from './toolbar';
+import { FilterBar } from './components/filter_bar';
import { DocumentTitle } from '../../../components/document_title';
import { NoIndices } from '../../../components/empty_states/no_indices';
@@ -56,7 +56,7 @@ export const SnapshotPage = () => {
) : metricIndicesExist ? (
<>
-
+
>
) : hasFailedLoadingSource ? (
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts
new file mode 100644
index 00000000000000..acd71e51376948
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import { createFormatter } from '../../../../utils/formatters';
+import { InfraFormatterType } from '../../../../lib/lib';
+import {
+ SnapshotMetricInput,
+ SnapshotCustomMetricInputRT,
+} from '../../../../../common/http_api/snapshot_api';
+import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric';
+
+interface MetricFormatter {
+ formatter: InfraFormatterType;
+ template: string;
+ bounds?: { min: number; max: number };
+}
+
+interface MetricFormatters {
+ [key: string]: MetricFormatter;
+}
+
+const METRIC_FORMATTERS: MetricFormatters = {
+ ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' },
+ ['cpu']: {
+ formatter: InfraFormatterType.percent,
+ template: '{{value}}',
+ },
+ ['memory']: {
+ formatter: InfraFormatterType.percent,
+ template: '{{value}}',
+ },
+ ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
+ ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
+ ['logRate']: {
+ formatter: InfraFormatterType.abbreviatedNumber,
+ template: '{{value}}/s',
+ },
+ ['diskIOReadBytes']: {
+ formatter: InfraFormatterType.bytes,
+ template: '{{value}}/s',
+ },
+ ['diskIOWriteBytes']: {
+ formatter: InfraFormatterType.bytes,
+ template: '{{value}}/s',
+ },
+ ['s3BucketSize']: {
+ formatter: InfraFormatterType.bytes,
+ template: '{{value}}',
+ },
+ ['s3TotalRequests']: {
+ formatter: InfraFormatterType.abbreviatedNumber,
+ template: '{{value}}',
+ },
+ ['s3NumberOfObjects']: {
+ formatter: InfraFormatterType.abbreviatedNumber,
+ template: '{{value}}',
+ },
+ ['s3UploadBytes']: {
+ formatter: InfraFormatterType.bytes,
+ template: '{{value}}',
+ },
+ ['s3DownloadBytes']: {
+ formatter: InfraFormatterType.bytes,
+ template: '{{value}}',
+ },
+ ['sqsOldestMessage']: {
+ formatter: InfraFormatterType.number,
+ template: '{{value}} seconds',
+ },
+};
+
+export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => (
+ val: string | number
+) => {
+ if (SnapshotCustomMetricInputRT.is(metric)) {
+ const formatter = createFormatterForMetric(metric);
+ return formatter(val);
+ }
+ const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count);
+ if (val == null) {
+ return '';
+ }
+ const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
+ return formatter(val);
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
index 81971bd31a9739..6913f67bad08a0 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx
@@ -17,7 +17,6 @@ import {
MetricsExplorerTimeOptions,
MetricsExplorerChartOptions,
} from '../hooks/use_metrics_explorer_options';
-import { Toolbar } from '../../../../components/eui/toolbar';
import { MetricsExplorerKueryBar } from './kuery_bar';
import { MetricsExplorerMetrics } from './metrics';
import { MetricsExplorerGroupBy } from './group_by';
@@ -28,6 +27,7 @@ import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state';
import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view';
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges';
+import { ToolbarPanel } from '../../../../components/toolbar_panel';
interface Props {
derivedIndexPattern: IIndexPattern;
@@ -65,7 +65,7 @@ export const MetricsExplorerToolbar = ({
const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges);
return (
-
+
-
+
);
};
diff --git a/x-pack/plugins/infra/public/utils/is_displayable.test.ts b/x-pack/plugins/infra/public/utils/is_displayable.test.ts
deleted file mode 100644
index ebd5c07327e9b7..00000000000000
--- a/x-pack/plugins/infra/public/utils/is_displayable.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { isDisplayable } from './is_displayable';
-
-describe('isDisplayable()', () => {
- test('field that is not displayable', () => {
- const field = {
- name: 'some.field',
- type: 'number',
- displayable: false,
- };
- expect(isDisplayable(field)).toBe(false);
- });
- test('field that is displayable', () => {
- const field = {
- name: 'some.field',
- type: 'number',
- displayable: true,
- };
- expect(isDisplayable(field)).toBe(true);
- });
- test('field that an ecs field', () => {
- const field = {
- name: '@timestamp',
- type: 'date',
- displayable: true,
- };
- expect(isDisplayable(field)).toBe(true);
- });
- test('field that matches same prefix', () => {
- const field = {
- name: 'system.network.name',
- type: 'string',
- displayable: true,
- };
- expect(isDisplayable(field, ['system.network'])).toBe(true);
- });
- test('field that does not matches same prefix', () => {
- const field = {
- name: 'system.load.1',
- type: 'number',
- displayable: true,
- };
- expect(isDisplayable(field, ['system.network'])).toBe(false);
- });
- test('field that is an K8s allowed field but does not match prefix', () => {
- const field = {
- name: 'kubernetes.namespace',
- type: 'string',
- displayable: true,
- };
- expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true);
- });
- test('field that is a Prometheus allowed field but does not match prefix', () => {
- const field = {
- name: 'prometheus.labels.foo.bar',
- type: 'string',
- displayable: true,
- };
- expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true);
- });
-});
diff --git a/x-pack/plugins/infra/public/utils/is_displayable.ts b/x-pack/plugins/infra/public/utils/is_displayable.ts
deleted file mode 100644
index 534282e807036d..00000000000000
--- a/x-pack/plugins/infra/public/utils/is_displayable.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { IFieldType } from 'src/plugins/data/public';
-import { startsWith, uniq } from 'lodash';
-import { getAllowedListForPrefix } from '../../common/ecs_allowed_list';
-
-interface DisplayableFieldType extends IFieldType {
- displayable?: boolean;
-}
-
-const fieldStartsWith = (field: DisplayableFieldType) => (name: string) =>
- startsWith(field.name, name);
-
-export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => {
- // We need to start with at least one prefix, even if it's empty
- const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : [''];
- // Create a set of allowed list based on the prefixes
- const allowedList = prefixes.reduce((acc, prefix) => {
- return uniq([...acc, ...getAllowedListForPrefix(prefix)]);
- }, [] as string[]);
- // If the field is displayable and part of the allowed list or covered by the prefix
- return (
- (field.displayable && prefixes.some(fieldStartsWith(field))) ||
- allowedList.some(fieldStartsWith(field))
- );
-};
diff --git a/x-pack/plugins/ingest_manager/common/constants/output.ts b/x-pack/plugins/ingest_manager/common/constants/output.ts
index 6060a2b63fc8ea..4c22d0e3fe7a30 100644
--- a/x-pack/plugins/ingest_manager/common/constants/output.ts
+++ b/x-pack/plugins/ingest_manager/common/constants/output.ts
@@ -12,5 +12,4 @@ export const DEFAULT_OUTPUT = {
is_default: true,
type: OutputType.Elasticsearch,
hosts: [''],
- api_key: '',
};
diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts
index 616195b32f266a..98ca52651a2ae3 100644
--- a/x-pack/plugins/ingest_manager/common/constants/routes.ts
+++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts
@@ -5,9 +5,10 @@
*/
// Base API paths
export const API_ROOT = `/api/ingest_manager`;
+export const EPM_API_ROOT = `${API_ROOT}/epm`;
+export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`;
export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`;
export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`;
-export const EPM_API_ROOT = `${API_ROOT}/epm`;
export const FLEET_API_ROOT = `${API_ROOT}/fleet`;
// EPM API routes
@@ -23,6 +24,11 @@ export const EPM_API_ROUTES = {
CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`,
};
+// Data stream API routes
+export const DATA_STREAM_API_ROUTES = {
+ LIST_PATTERN: `${DATA_STREAM_API_ROOT}`,
+};
+
// Datasource API routes
export const DATASOURCE_API_ROUTES = {
LIST_PATTERN: `${DATASOURCE_API_ROOT}`,
diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts
index f2343b1039151f..46b76d886f3cd3 100644
--- a/x-pack/plugins/ingest_manager/common/services/routes.ts
+++ b/x-pack/plugins/ingest_manager/common/services/routes.ts
@@ -8,6 +8,7 @@ import {
EPM_API_ROUTES,
DATASOURCE_API_ROUTES,
AGENT_CONFIG_API_ROUTES,
+ DATA_STREAM_API_ROUTES,
FLEET_SETUP_API_ROUTES,
AGENT_API_ROUTES,
ENROLLMENT_API_KEY_ROUTES,
@@ -88,6 +89,12 @@ export const agentConfigRouteService = {
},
};
+export const dataStreamRouteService = {
+ getListPath: () => {
+ return DATA_STREAM_API_ROUTES.LIST_PATTERN;
+ },
+};
+
export const fleetSetupRouteService = {
getFleetSetupPath: () => FLEET_SETUP_API_ROUTES.INFO_PATTERN,
postFleetSetupPath: () => FLEET_SETUP_API_ROUTES.CREATE_PATTERN,
diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts
index 438db9a25b8ee4..42f7a9333118e5 100644
--- a/x-pack/plugins/ingest_manager/common/types/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/index.ts
@@ -3,43 +3,9 @@
* 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 { AgentStatus } from './models';
-
export * from './models';
export * from './rest_spec';
-/**
- * Service to return the index pattern of EPM packages
- */
-export interface ESIndexPatternService {
- getESIndexPattern(
- savedObjectsClient: SavedObjectsClientContract,
- pkgName: string,
- datasetPath: string
- ): Promise;
-}
-
-/**
- * Describes public IngestManager plugin contract returned at the `startup` stage.
- */
-export interface IngestManagerStartupContract {
- esIndexPatternService: ESIndexPatternService;
- agentService: AgentService;
-}
-
-/**
- * A service that provides exported functions that return information about an Agent
- */
-export interface AgentService {
- /**
- * Return the status by the Agent's id
- * @param soClient
- * @param agentId
- */
- getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise;
-}
-
export interface IngestManagerConfigType {
enabled: boolean;
epm: {
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index 14b2b2e47d17fd..fcd3955f3a32fc 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -20,15 +20,18 @@ export interface NewAgentAction {
sent_at?: string;
}
-export type AgentAction = NewAgentAction & {
+export interface AgentAction extends NewAgentAction {
id: string;
agent_id: string;
created_at: string;
-} & SavedObjectAttributes;
+}
-export interface AgentActionSOAttributes extends NewAgentAction, SavedObjectAttributes {
+export interface AgentActionSOAttributes extends SavedObjectAttributes {
+ type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
+ sent_at?: string;
created_at: string;
agent_id: string;
+ data?: string;
}
export interface AgentEvent {
@@ -64,6 +67,7 @@ interface AgentBase {
shared_id?: string;
access_api_key_id?: string;
default_api_key?: string;
+ default_api_key_id?: string;
config_id?: string;
config_revision?: number | null;
config_newest_revision?: number;
diff --git a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts
new file mode 100644
index 00000000000000..7da9bbad1b1700
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface DataStream {
+ index: string;
+ dataset: string;
+ namespace: string;
+ type: string;
+ package: string;
+ last_activity: string;
+ size_in_bytes: number;
+}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts
index 579b510e52daae..f73ab7af636a90 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts
@@ -7,6 +7,7 @@
export * from './agent';
export * from './agent_config';
export * from './datasource';
+export * from './data_stream';
export * from './output';
export * from './epm';
export * from './enrollment_api_key';
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts
similarity index 59%
rename from x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts
rename to x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts
index 2244bcd44043f4..24f8110562bfcf 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts
@@ -3,11 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { DataStream } from '../models';
-export const GetFleetSetupRequestSchema = {};
-
-export const CreateFleetSetupRequestSchema = {};
-
-export interface CreateFleetSetupResponse {
- isInitialized: boolean;
+export interface GetDataStreamsResponse {
+ data_streams: DataStream[];
}
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts
index dc1d748a8743ac..c4ba8ee595acfc 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts
@@ -4,16 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface GetFleetSetupRequest {}
-
-export interface CreateFleetSetupRequest {
- body: {
- fleet_enroll_username: string;
- fleet_enroll_password: string;
- };
-}
-
export interface CreateFleetSetupResponse {
isInitialized: boolean;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
index abe1bc8e3eddba..c1805023f497a8 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
@@ -5,6 +5,7 @@
*/
export * from './common';
export * from './datasource';
+export * from './data_stream';
export * from './agent';
export * from './agent_config';
export * from './fleet_setup';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts
index 282ea8dbee3a28..619d03651dd961 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts
@@ -12,6 +12,7 @@ export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`;
export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`;
export const AGENT_CONFIG_PATH = '/configs';
export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`;
+export const DATA_STREAM_PATH = '/data-streams';
export const FLEET_PATH = '/fleet';
export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`;
export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts
new file mode 100644
index 00000000000000..9acf4b1e174496
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { useRequest } from './use_request';
+import { dataStreamRouteService } from '../../services';
+import { GetDataStreamsResponse } from '../../types';
+
+export const useGetDataStreams = () => {
+ return useRequest({
+ path: dataStreamRouteService.getListPath(),
+ method: 'get',
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts
index 5014049407e65d..084aba9a34309d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts
@@ -6,6 +6,7 @@
export { setHttpClient, sendRequest, useRequest } from './use_request';
export * from './agent_config';
export * from './datasource';
+export * from './data_stream';
export * from './agents';
export * from './enrollment_api_keys';
export * from './epm';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
index f7c2805c6ea7c8..6485862830d8ab 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
@@ -16,10 +16,10 @@ import {
IngestManagerConfigType,
IngestManagerStartDeps,
} from '../../plugin';
-import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants';
+import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants';
import { DefaultLayout, WithoutHeaderLayout } from './layouts';
import { Loading, Error } from './components';
-import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections';
+import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections';
import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks';
import { PackageInstallProvider } from './sections/epm/hooks';
import { sendSetup } from './hooks/use_request/setup';
@@ -98,6 +98,11 @@ const IngestManagerRoutes = ({ ...rest }) => {
+
+
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
index 345fd535b8eccd..f1f9063de72f00 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
@@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Section } from '../sections';
import { AlphaMessaging } from '../components';
import { useLink, useConfig } from '../hooks';
-import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants';
+import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants';
interface Props {
section?: Section;
@@ -76,6 +76,12 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre
defaultMessage="Fleet"
/>
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index 0498e814440c71..1ea162252c741e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
@@ -191,7 +191,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Name',
}),
width: '20%',
- // FIXME: use version once available - see: https://github.com/elastic/kibana/issues/56750
render: (name: string, agentConfig: AgentConfig) => (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx
new file mode 100644
index 00000000000000..7b0641e66fd43e
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.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 { HashRouter as Router, Route, Switch } from 'react-router-dom';
+import { DataStreamListPage } from './list_page';
+
+export const DataStreamApp: React.FunctionComponent = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx
new file mode 100644
index 00000000000000..d7a3e933f3bb52
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx
@@ -0,0 +1,283 @@
+/*
+ * 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, { useMemo } from 'react';
+import {
+ EuiBadge,
+ EuiButton,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiEmptyPrompt,
+ EuiInMemoryTable,
+ EuiTableActionsColumnType,
+ EuiTableFieldDataColumnType,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
+import { DataStream } from '../../../types';
+import { WithHeaderLayout } from '../../../layouts';
+import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks';
+
+const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ {children}
+
+);
+
+export const DataStreamListPage: React.FunctionComponent<{}> = () => {
+ const {
+ data: { fieldFormats },
+ } = useStartDeps();
+
+ const { pagination, pageSizeOptions } = usePagination();
+
+ // Fetch agent configs
+ const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams();
+
+ // Some configs retrieved, set up table props
+ const columns = useMemo(() => {
+ const cols: Array<
+ EuiTableFieldDataColumnType | EuiTableActionsColumnType
+ > = [
+ {
+ field: 'dataset',
+ sortable: true,
+ width: '25%',
+ truncateText: true,
+ name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', {
+ defaultMessage: 'Dataset',
+ }),
+ },
+ {
+ field: 'type',
+ sortable: true,
+ truncateText: true,
+ name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', {
+ defaultMessage: 'Type',
+ }),
+ },
+ {
+ field: 'namespace',
+ sortable: true,
+ truncateText: true,
+ name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', {
+ defaultMessage: 'Namespace',
+ }),
+ render: (namespace: string) => {
+ return namespace ? {namespace} : '';
+ },
+ },
+ {
+ field: 'package',
+ sortable: true,
+ truncateText: true,
+ name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', {
+ defaultMessage: 'Integration',
+ }),
+ },
+ {
+ field: 'last_activity',
+ sortable: true,
+ width: '25%',
+ dataType: 'date',
+ name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', {
+ defaultMessage: 'Last activity',
+ }),
+ render: (date: DataStream['last_activity']) => {
+ try {
+ const formatter = fieldFormats.getInstance('date');
+ return formatter.convert(date);
+ } catch (e) {
+ return ;
+ }
+ },
+ },
+ {
+ field: 'size_in_bytes',
+ sortable: true,
+ name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', {
+ defaultMessage: 'Size',
+ }),
+ render: (size: DataStream['size_in_bytes']) => {
+ try {
+ const formatter = fieldFormats.getInstance('bytes');
+ return formatter.convert(size);
+ } catch (e) {
+ return `${size}b`;
+ }
+ },
+ },
+ ];
+ return cols;
+ }, [fieldFormats]);
+
+ const emptyPrompt = useMemo(
+ () => (
+
+
+
+ }
+ />
+ ),
+ []
+ );
+
+ const filterOptions: { [key: string]: string[] } = {
+ dataset: [],
+ type: [],
+ namespace: [],
+ package: [],
+ };
+
+ if (dataStreamsData && dataStreamsData.data_streams.length) {
+ dataStreamsData.data_streams.forEach(stream => {
+ const { dataset, type, namespace, package: pkg } = stream;
+ if (!filterOptions.dataset.includes(dataset)) {
+ filterOptions.dataset.push(dataset);
+ }
+ if (!filterOptions.type.includes(type)) {
+ filterOptions.type.push(type);
+ }
+ if (!filterOptions.namespace.includes(namespace)) {
+ filterOptions.namespace.push(namespace);
+ }
+ if (!filterOptions.package.includes(pkg)) {
+ filterOptions.package.push(pkg);
+ }
+ });
+ }
+
+ return (
+
+
+ ) : dataStreamsData && !dataStreamsData.data_streams.length ? (
+ emptyPrompt
+ ) : (
+
+ )
+ }
+ items={dataStreamsData ? dataStreamsData.data_streams : []}
+ itemId="index"
+ columns={columns}
+ pagination={{
+ initialPageSize: pagination.pageSize,
+ pageSizeOptions,
+ }}
+ sorting={true}
+ search={{
+ toolsRight: [
+ sendRequest()}>
+
+ ,
+ ],
+ box: {
+ placeholder: i18n.translate(
+ 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle',
+ {
+ defaultMessage: 'Filter data streams',
+ }
+ ),
+ incremental: true,
+ },
+ filters: [
+ {
+ type: 'field_value_selection',
+ field: 'dataset',
+ name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', {
+ defaultMessage: 'Dataset',
+ }),
+ multiSelect: 'or',
+ options: filterOptions.dataset.map(option => ({
+ value: option,
+ name: option,
+ })),
+ },
+ {
+ type: 'field_value_selection',
+ field: 'type',
+ name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', {
+ defaultMessage: 'Type',
+ }),
+ multiSelect: 'or',
+ options: filterOptions.type.map(option => ({
+ value: option,
+ name: option,
+ })),
+ },
+ {
+ type: 'field_value_selection',
+ field: 'namespace',
+ name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', {
+ defaultMessage: 'Namespace',
+ }),
+ multiSelect: 'or',
+ options: filterOptions.namespace.map(option => ({
+ value: option,
+ name: option,
+ })),
+ },
+ {
+ type: 'field_value_selection',
+ field: 'package',
+ name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', {
+ defaultMessage: 'Integration',
+ }),
+ multiSelect: 'or',
+ options: filterOptions.package.map(option => ({
+ value: option,
+ name: option,
+ })),
+ },
+ ],
+ }}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx
index c691bb609d4354..1f46c4cc820cb9 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx
@@ -6,6 +6,7 @@
export { IngestManagerOverview } from './overview';
export { EPMApp } from './epm';
export { AgentConfigApp } from './agent_config';
+export { DataStreamApp } from './data_stream';
export { FleetApp } from './fleet';
-export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet';
+export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet' | 'data_stream';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
index 5ebd1300baf65d..53dbe295718c50 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
@@ -9,6 +9,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/utils';
export {
agentConfigRouteService,
datasourceRouteService,
+ dataStreamRouteService,
fleetSetupRouteService,
agentRouteService,
enrollmentAPIKeyRouteService,
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
index 75194d3397f90b..8ca1495a94071d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
@@ -17,6 +17,7 @@ export {
DatasourceInput,
DatasourceInputStream,
DatasourceConfigRecordEntry,
+ DataStream,
// API schemas - Agent Config
GetAgentConfigsResponse,
GetAgentConfigsResponseItem,
@@ -30,6 +31,8 @@ export {
// API schemas - Datasource
CreateDatasourceRequest,
CreateDatasourceResponse,
+ // API schemas - Data Streams
+ GetDataStreamsResponse,
// API schemas - Agents
GetAgentsResponse,
GetAgentsRequest,
diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts
index 6ac92ca5d2a912..b2e72fefe59978 100644
--- a/x-pack/plugins/ingest_manager/server/constants/index.ts
+++ b/x-pack/plugins/ingest_manager/server/constants/index.ts
@@ -12,6 +12,7 @@ export {
// Routes
PLUGIN_ID,
EPM_API_ROUTES,
+ DATA_STREAM_API_ROUTES,
DATASOURCE_API_ROUTES,
AGENT_API_ROUTES,
AGENT_CONFIG_API_ROUTES,
diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts
index 0f49de0d5fdd11..851a58f5adac28 100644
--- a/x-pack/plugins/ingest_manager/server/index.ts
+++ b/x-pack/plugins/ingest_manager/server/index.ts
@@ -6,6 +6,12 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from 'src/core/server';
import { IngestManagerPlugin } from './plugin';
+export { AgentService, ESIndexPatternService } from './services';
+export {
+ IngestManagerSetupContract,
+ IngestManagerSetupDeps,
+ IngestManagerStartContract,
+} from './plugin';
export const config = {
exposeToBrowser: {
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 80e35eadb46037..55aea4b1a4cdd2 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -11,11 +11,12 @@ import {
Plugin,
PluginInitializerContext,
SavedObjectsServiceStart,
- RecursiveReadonly,
} from 'kibana/server';
-import { deepFreeze } from '../../../../src/core/utils';
import { LicensingPluginSetup } from '../../licensing/server';
-import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server';
+import {
+ EncryptedSavedObjectsPluginStart,
+ EncryptedSavedObjectsPluginSetup,
+} from '../../encrypted_saved_objects/server';
import { SecurityPluginSetup } from '../../security/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import {
@@ -28,10 +29,11 @@ import {
AGENT_EVENT_SAVED_OBJECT_TYPE,
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
} from './constants';
-
+import { registerEncryptedSavedObjects } from './saved_objects';
import {
registerEPMRoutes,
registerDatasourceRoutes,
+ registerDataStreamRoutes,
registerAgentConfigRoutes,
registerSetupRoutes,
registerAgentRoutes,
@@ -39,16 +41,20 @@ import {
registerInstallScriptRoutes,
} from './routes';
-import { IngestManagerConfigType, IngestManagerStartupContract } from '../common';
+import { IngestManagerConfigType } from '../common';
import { appContextService, ESIndexPatternSavedObjectService } from './services';
+import { ESIndexPatternService, AgentService } from './services';
import { getAgentStatusById } from './services/agents';
export interface IngestManagerSetupDeps {
licensing: LicensingPluginSetup;
security?: SecurityPluginSetup;
features?: FeaturesPluginSetup;
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
}
+export type IngestManagerStartDeps = object;
+
export interface IngestManagerAppContext {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
security?: SecurityPluginSetup;
@@ -56,6 +62,8 @@ export interface IngestManagerAppContext {
savedObjects: SavedObjectsServiceStart;
}
+export type IngestManagerSetupContract = void;
+
const allSavedObjectTypes = [
OUTPUT_SAVED_OBJECT_TYPE,
AGENT_CONFIG_SAVED_OBJECT_TYPE,
@@ -66,7 +74,22 @@ const allSavedObjectTypes = [
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
];
-export class IngestManagerPlugin implements Plugin {
+/**
+ * Describes public IngestManager plugin contract returned at the `startup` stage.
+ */
+export interface IngestManagerStartContract {
+ esIndexPatternService: ESIndexPatternService;
+ agentService: AgentService;
+}
+
+export class IngestManagerPlugin
+ implements
+ Plugin<
+ IngestManagerSetupContract,
+ IngestManagerStartContract,
+ IngestManagerSetupDeps,
+ IngestManagerStartDeps
+ > {
private config$: Observable;
private security: SecurityPluginSetup | undefined;
@@ -79,6 +102,8 @@ export class IngestManagerPlugin implements Plugin> {
+ ) {
appContextService.start({
encryptedSavedObjects: plugins.encryptedSavedObjects,
security: this.security,
config$: this.config$,
savedObjects: core.savedObjects,
});
- return deepFreeze({
+ return {
esIndexPatternService: new ESIndexPatternSavedObjectService(),
agentService: {
getAgentStatusById,
},
- });
+ };
}
public async stop() {
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts
index 76247c338a24f5..bcb9a7797f26ab 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts
@@ -10,8 +10,7 @@ import {
RequestHandlerContext,
SavedObjectsClientContract,
} from 'kibana/server';
-import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
-import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
+import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks';
import { ActionsService } from '../../services/agents';
import { AgentAction } from '../../../common/types/models';
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts
new file mode 100644
index 00000000000000..a24518d644c4c7
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 { RequestHandler } from 'src/core/server';
+import { DataStream } from '../../types';
+import { GetDataStreamsResponse } from '../../../common';
+
+const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*';
+
+export const getListHandler: RequestHandler = async (context, request, response) => {
+ const callCluster = context.core.elasticsearch.dataClient.callAsCurrentUser;
+
+ try {
+ // Get stats (size on disk) of all potentially matching indices
+ const { indices: indexStats } = await callCluster('indices.stats', {
+ index: DATA_STREAM_INDEX_PATTERN,
+ metric: ['store'],
+ });
+
+ // Get all matching indices and info about each
+ // This returns the top 100,000 indices (as buckets) by last activity
+ const {
+ aggregations: {
+ index: { buckets: indexResults },
+ },
+ } = await callCluster('search', {
+ index: DATA_STREAM_INDEX_PATTERN,
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'stream.namespace',
+ },
+ },
+ {
+ exists: {
+ field: 'stream.dataset',
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ index: {
+ terms: {
+ field: '_index',
+ size: 100000,
+ order: {
+ last_activity: 'desc',
+ },
+ },
+ aggs: {
+ dataset: {
+ terms: {
+ field: 'stream.dataset',
+ size: 1,
+ },
+ },
+ namespace: {
+ terms: {
+ field: 'stream.namespace',
+ size: 1,
+ },
+ },
+ type: {
+ terms: {
+ field: 'stream.type',
+ size: 1,
+ },
+ },
+ package: {
+ terms: {
+ field: 'event.module',
+ size: 1,
+ },
+ },
+ last_activity: {
+ max: {
+ field: '@timestamp',
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const dataStreams: DataStream[] = (indexResults as any[]).map(result => {
+ const {
+ key: indexName,
+ dataset: { buckets: datasetBuckets },
+ namespace: { buckets: namespaceBuckets },
+ type: { buckets: typeBuckets },
+ package: { buckets: packageBuckets },
+ last_activity: { value_as_string: lastActivity },
+ } = result;
+ return {
+ index: indexName,
+ dataset: datasetBuckets.length ? datasetBuckets[0].key : '',
+ namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '',
+ type: typeBuckets.length ? typeBuckets[0].key : '',
+ package: packageBuckets.length ? packageBuckets[0].key : '',
+ last_activity: lastActivity,
+ size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0,
+ };
+ });
+
+ const body: GetDataStreamsResponse = {
+ data_streams: dataStreams,
+ };
+ return response.ok({
+ body,
+ });
+ } catch (e) {
+ return response.customError({
+ statusCode: 500,
+ body: { message: e.message },
+ });
+ }
+};
diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts
new file mode 100644
index 00000000000000..39502eba89a6a4
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts
@@ -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 { IRouter } from 'src/core/server';
+import { PLUGIN_ID, DATA_STREAM_API_ROUTES } from '../../constants';
+import { getListHandler } from './handlers';
+
+export const registerRoutes = (router: IRouter) => {
+ // List of data streams
+ router.get(
+ {
+ path: DATA_STREAM_API_ROUTES.LIST_PATTERN,
+ validate: false,
+ options: { tags: [`access:${PLUGIN_ID}-read`] },
+ },
+ getListHandler
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts
index 33d75f3ab82cda..8a186c54850245 100644
--- a/x-pack/plugins/ingest_manager/server/routes/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/index.ts
@@ -5,6 +5,7 @@
*/
export { registerRoutes as registerAgentConfigRoutes } from './agent_config';
export { registerRoutes as registerDatasourceRoutes } from './datasource';
+export { registerRoutes as registerDataStreamRoutes } from './data_streams';
export { registerRoutes as registerEPMRoutes } from './epm';
export { registerRoutes as registerSetupRoutes } from './setup';
export { registerRoutes as registerAgentRoutes } from './agent';
diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
index 5c66f9008e2a38..837e73b966feba 100644
--- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
@@ -5,7 +5,7 @@
*/
import { RequestHandler } from 'src/core/server';
import { outputService } from '../../services';
-import { CreateFleetSetupResponse } from '../../types';
+import { CreateFleetSetupResponse } from '../../../common';
import { setupIngestManager, setupFleet } from '../../services/setup';
export const getFleetSetupHandler: RequestHandler = async (context, request, response) => {
diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts
index a2c641503e825b..edc9a0a268161e 100644
--- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts
@@ -5,7 +5,6 @@
*/
import { IRouter } from 'src/core/server';
import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants';
-import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types';
import {
getFleetSetupHandler,
createFleetSetupHandler,
@@ -28,7 +27,7 @@ export const registerRoutes = (router: IRouter) => {
router.get(
{
path: FLEET_SETUP_API_ROUTES.INFO_PATTERN,
- validate: GetFleetSetupRequestSchema,
+ validate: false,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getFleetSetupHandler
@@ -38,7 +37,7 @@ export const registerRoutes = (router: IRouter) => {
router.post(
{
path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN,
- validate: CreateFleetSetupRequestSchema,
+ validate: false,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
createFleetSetupHandler
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts
index 0a7229b1f28072..37a00228443e15 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts
@@ -13,6 +13,7 @@ import {
AGENT_ACTION_SAVED_OBJECT_TYPE,
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
} from './constants';
+import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server';
/*
* Saved object mappings
@@ -35,7 +36,7 @@ export const savedObjectMappings = {
last_checkin: { type: 'date' },
config_revision: { type: 'integer' },
config_newest_revision: { type: 'integer' },
- // FIXME_INGEST https://github.com/elastic/kibana/issues/56554
+ default_api_key_id: { type: 'keyword' },
default_api_key: { type: 'keyword' },
updated_at: { type: 'date' },
current_error_events: { type: 'text' },
@@ -45,8 +46,7 @@ export const savedObjectMappings = {
properties: {
agent_id: { type: 'keyword' },
type: { type: 'keyword' },
- // FIXME_INGEST https://github.com/elastic/kibana/issues/56554
- data: { type: 'flattened' },
+ data: { type: 'binary' },
sent_at: { type: 'date' },
created_at: { type: 'date' },
},
@@ -83,7 +83,6 @@ export const savedObjectMappings = {
properties: {
name: { type: 'keyword' },
type: { type: 'keyword' },
- // FIXME_INGEST https://github.com/elastic/kibana/issues/56554
api_key: { type: 'binary' },
api_key_id: { type: 'keyword' },
config_id: { type: 'keyword' },
@@ -100,8 +99,6 @@ export const savedObjectMappings = {
is_default: { type: 'boolean' },
hosts: { type: 'keyword' },
ca_sha256: { type: 'keyword' },
- // FIXME_INGEST https://github.com/elastic/kibana/issues/56554
- api_key: { type: 'keyword' },
fleet_enroll_username: { type: 'binary' },
fleet_enroll_password: { type: 'binary' },
config: { type: 'flattened' },
@@ -165,3 +162,61 @@ export const savedObjectMappings = {
},
},
};
+
+export function registerEncryptedSavedObjects(
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
+) {
+ // Encrypted saved objects
+ encryptedSavedObjects.registerType({
+ type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
+ attributesToEncrypt: new Set(['api_key']),
+ attributesToExcludeFromAAD: new Set([
+ 'name',
+ 'type',
+ 'api_key_id',
+ 'config_id',
+ 'created_at',
+ 'updated_at',
+ 'expire_at',
+ 'active',
+ ]),
+ });
+ encryptedSavedObjects.registerType({
+ type: OUTPUT_SAVED_OBJECT_TYPE,
+ attributesToEncrypt: new Set(['fleet_enroll_username', 'fleet_enroll_password']),
+ attributesToExcludeFromAAD: new Set([
+ 'name',
+ 'type',
+ 'is_default',
+ 'hosts',
+ 'ca_sha256',
+ 'config',
+ ]),
+ });
+ encryptedSavedObjects.registerType({
+ type: AGENT_SAVED_OBJECT_TYPE,
+ attributesToEncrypt: new Set(['default_api_key']),
+ attributesToExcludeFromAAD: new Set([
+ 'shared_id',
+ 'type',
+ 'active',
+ 'enrolled_at',
+ 'access_api_key_id',
+ 'version',
+ 'user_provided_metadata',
+ 'local_metadata',
+ 'config_id',
+ 'last_updated',
+ 'last_checkin',
+ 'config_revision',
+ 'config_newest_revision',
+ 'updated_at',
+ 'current_error_events',
+ ]),
+ });
+ encryptedSavedObjects.registerType({
+ type: AGENT_ACTION_SAVED_OBJECT_TYPE,
+ attributesToEncrypt: new Set(['data']),
+ attributesToExcludeFromAAD: new Set(['agent_id', 'type', 'sent_at', 'created_at']),
+ });
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
index b4c1f09015a69b..a8fada00e25da6 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
@@ -5,7 +5,9 @@
*/
import Boom from 'boom';
import { SavedObjectsBulkResponse } from 'kibana/server';
-import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { savedObjectsClientMock } from 'src/core/server/mocks';
+import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
+
import {
Agent,
AgentAction,
@@ -14,10 +16,31 @@ import {
} from '../../../common/types/models';
import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
import { acknowledgeAgentActions } from './acks';
+import { appContextService } from '../app_context';
+import { IngestManagerAppContext } from '../../plugin';
describe('test agent acks services', () => {
it('should succeed on valid and matched actions', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();
+ const mockStartEncryptedSOClient = encryptedSavedObjectsMock.createStart();
+ appContextService.start(({
+ encryptedSavedObjects: mockStartEncryptedSOClient,
+ } as unknown) as IngestManagerAppContext);
+
+ mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue(
+ Promise.resolve({
+ id: 'action1',
+ references: [],
+ type: 'agent_actions',
+ attributes: {
+ type: 'CONFIG_CHANGE',
+ agent_id: 'id',
+ sent_at: '2020-03-14T19:45:02.620Z',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ created_at: '2020-03-14T19:45:02.620Z',
+ },
+ })
+ );
mockSavedObjectsClient.bulkGet.mockReturnValue(
Promise.resolve({
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts
index f2e671c6dbaa87..c7390079523893 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts
@@ -6,17 +6,17 @@
import { createAgentAction } from './actions';
import { SavedObject } from 'kibana/server';
-import { AgentAction, AgentActionSOAttributes } from '../../../common/types/models';
-import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { AgentAction } from '../../../common/types/models';
+import { savedObjectsClientMock } from 'src/core/server/mocks';
describe('test agent actions services', () => {
it('should create a new action', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();
- const newAgentAction: AgentActionSOAttributes = {
+ const newAgentAction: Omit = {
agent_id: 'agentid',
type: 'CONFIG_CHANGE',
- data: 'data',
+ data: { content: 'data' },
sent_at: '2020-03-14T19:45:02.620Z',
created_at: '2020-03-14T19:45:02.620Z',
};
@@ -31,7 +31,7 @@ describe('test agent actions services', () => {
.calls[0][1] as unknown) as AgentAction;
expect(createdAction).toBeDefined();
expect(createdAction?.type).toEqual(newAgentAction.type);
- expect(createdAction?.data).toEqual(newAgentAction.data);
+ expect(createdAction?.data).toEqual(JSON.stringify(newAgentAction.data));
expect(createdAction?.sent_at).toEqual(newAgentAction.sent_at);
});
});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
index a8ef0820f8d9f3..1bb177e54282d0 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
@@ -8,16 +8,21 @@ import { SavedObjectsClientContract } from 'kibana/server';
import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/types/models';
import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants';
import { savedObjectToAgentAction } from './saved_objects';
+import { appContextService } from '../app_context';
export async function createAgentAction(
soClient: SavedObjectsClientContract,
- newAgentAction: AgentActionSOAttributes
+ newAgentAction: Omit
): Promise {
const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, {
...newAgentAction,
+ data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined,
});
- return savedObjectToAgentAction(so);
+ const agentAction = savedObjectToAgentAction(so);
+ agentAction.data = newAgentAction.data;
+
+ return agentAction;
}
export async function getAgentActionsForCheckin(
@@ -29,21 +34,47 @@ export async function getAgentActionsForCheckin(
filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`,
});
- return res.saved_objects.map(savedObjectToAgentAction);
+ return Promise.all(
+ res.saved_objects.map(async so => {
+ // Get decrypted actions
+ return savedObjectToAgentAction(
+ await appContextService
+ .getEncryptedSavedObjects()
+ .getDecryptedAsInternalUser(
+ AGENT_ACTION_SAVED_OBJECT_TYPE,
+ so.id
+ )
+ );
+ })
+ );
}
export async function getAgentActionByIds(
soClient: SavedObjectsClientContract,
actionIds: string[]
) {
- const res = await soClient.bulkGet(
- actionIds.map(actionId => ({
- id: actionId,
- type: AGENT_ACTION_SAVED_OBJECT_TYPE,
- }))
- );
+ const actions = (
+ await soClient.bulkGet(
+ actionIds.map(actionId => ({
+ id: actionId,
+ type: AGENT_ACTION_SAVED_OBJECT_TYPE,
+ }))
+ )
+ ).saved_objects.map(savedObjectToAgentAction);
- return res.saved_objects.map(savedObjectToAgentAction);
+ return Promise.all(
+ actions.map(async action => {
+ // Get decrypted actions
+ return savedObjectToAgentAction(
+ await appContextService
+ .getEncryptedSavedObjects()
+ .getDecryptedAsInternalUser(
+ AGENT_ACTION_SAVED_OBJECT_TYPE,
+ action.id
+ )
+ );
+ })
+ );
}
export interface ActionsService {
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts
index ec10ca6e77e05f..72a86d7c8158e0 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts
@@ -53,12 +53,12 @@ describe('Agent checkin service', () => {
agent_id: 'agent1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
- data: JSON.stringify({
+ data: {
config: {
id: 'config1',
revision: 2,
},
- }),
+ },
},
]
);
@@ -80,24 +80,24 @@ describe('Agent checkin service', () => {
agent_id: 'agent1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
- data: JSON.stringify({
+ data: {
config: {
id: 'config2',
revision: 2,
},
- }),
+ },
},
{
id: 'action1',
agent_id: 'agent1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
- data: JSON.stringify({
+ data: {
config: {
id: 'config1',
revision: 1,
},
- }),
+ },
},
]
);
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
index 2873aad7f691aa..c96a81ed9b7587 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
@@ -17,6 +17,7 @@ import { agentConfigService } from '../agent_config';
import * as APIKeysService from '../api_keys';
import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants';
import { getAgentActionsForCheckin, createAgentAction } from './actions';
+import { appContextService } from '../app_context';
export async function agentCheckin(
soClient: SavedObjectsClientContract,
@@ -27,7 +28,6 @@ export async function agentCheckin(
const updateData: {
last_checkin: string;
default_api_key?: string;
- actions?: AgentAction[];
local_metadata?: string;
current_error_events?: string;
} = {
@@ -38,11 +38,17 @@ export async function agentCheckin(
// Generate new agent config if config is updated
if (agent.config_id && shouldCreateConfigAction(agent, actions)) {
+ const {
+ attributes: { default_api_key: defaultApiKey },
+ } = await appContextService
+ .getEncryptedSavedObjects()
+ .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id);
+
const config = await agentConfigService.getFullConfig(soClient, agent.config_id);
if (config) {
// Assign output API keys
// We currently only support default ouput
- if (!agent.default_api_key) {
+ if (!defaultApiKey) {
updateData.default_api_key = await APIKeysService.generateOutputApiKey(
soClient,
'default',
@@ -50,7 +56,7 @@ export async function agentCheckin(
);
}
// Mutate the config to set the api token for this agent
- config.outputs.default.api_key = agent.default_api_key || updateData.default_api_key;
+ config.outputs.default.api_key = defaultApiKey || updateData.default_api_key;
const configChangeAction = await createAgentAction(soClient, {
agent_id: agent.id,
@@ -62,9 +68,6 @@ export async function agentCheckin(
actions.push(configChangeAction);
}
}
- if (localMetadata) {
- updateData.local_metadata = JSON.stringify(localMetadata);
- }
const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, events);
@@ -172,7 +175,7 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]):
return false;
}
- const data = JSON.parse(action.data);
+ const { data } = action;
return (
data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
index aa885207406872..b182662e0fb4e2 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
@@ -38,5 +38,6 @@ export function savedObjectToAgentAction(so: SavedObject(AGENT_SAVED_OBJECT_TYPE, agentId, {
diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts
index a6a2db8be4e9d9..c9ead09b0908d5 100644
--- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts
+++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts
@@ -10,6 +10,7 @@ import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types';
import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants';
import { createAPIKey, invalidateAPIKey } from './security';
import { agentConfigService } from '../agent_config';
+import { appContextService } from '../app_context';
export async function listEnrollmentApiKeys(
soClient: SavedObjectsClientContract,
@@ -45,9 +46,13 @@ export async function listEnrollmentApiKeys(
}
export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) {
- return savedObjectToEnrollmentApiKey(
- await soClient.get(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id)
- );
+ const so = await appContextService
+ .getEncryptedSavedObjects()
+ .getDecryptedAsInternalUser(
+ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
+ id
+ );
+ return savedObjectToEnrollmentApiKey(so);
}
/**
@@ -120,16 +125,19 @@ export async function generateEnrollmentAPIKey(
const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64');
- return savedObjectToEnrollmentApiKey(
- await soClient.create(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, {
+ const so = await soClient.create(
+ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
+ {
active: true,
api_key_id: key.id,
api_key: apiKey,
name,
config_id: configId,
created_at: new Date().toISOString(),
- })
+ }
);
+
+ return getEnrollmentAPIKey(soClient, so.id);
}
function savedObjectToEnrollmentApiKey({
diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts
index a0a7c8dd7c05a0..e917d2edd13090 100644
--- a/x-pack/plugins/ingest_manager/server/services/app_context.ts
+++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts
@@ -34,6 +34,9 @@ class AppContextService {
public stop() {}
public getEncryptedSavedObjects() {
+ if (!this.encryptedSavedObjects) {
+ throw new Error('Encrypted saved object start service not set.');
+ }
return this.encryptedSavedObjects;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts
index e2c27cdacda2fc..fc2fe6d1c40e8c 100644
--- a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts
+++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts
@@ -5,7 +5,7 @@
*/
import { SavedObjectsClientContract } from 'kibana/server';
import { getInstallation } from './epm/packages';
-import { ESIndexPatternService } from '../../common/types';
+import { ESIndexPatternService } from '../../server';
export class ESIndexPatternSavedObjectService implements ESIndexPatternService {
public async getESIndexPattern(
diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts
index 4dfc3cb58b7330..1b0f174cc1a8e7 100644
--- a/x-pack/plugins/ingest_manager/server/services/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/index.ts
@@ -3,9 +3,35 @@
* 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 { AgentStatus } from '../../common/types/models';
+
export { appContextService } from './app_context';
export { ESIndexPatternSavedObjectService } from './es_index_pattern';
+/**
+ * Service to return the index pattern of EPM packages
+ */
+export interface ESIndexPatternService {
+ getESIndexPattern(
+ savedObjectsClient: SavedObjectsClientContract,
+ pkgName: string,
+ datasetPath: string
+ ): Promise;
+}
+
+/**
+ * A service that provides exported functions that return information about an Agent
+ */
+export interface AgentService {
+ /**
+ * Return the status by the Agent's id
+ * @param soClient
+ * @param agentId
+ */
+ getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise;
+}
+
// Saved object services
export { datasourceService } from './datasource';
export { agentConfigService } from './agent_config';
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index 105f9039f1e98d..aa5496cc836b71 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -22,6 +22,7 @@ export {
AgentConfig,
NewAgentConfig,
AgentConfigStatus,
+ DataStream,
Output,
NewOutput,
OutputType,
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts
index c143cd3b35f91a..42b607fa1c7157 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts
@@ -9,5 +9,4 @@ export * from './agent';
export * from './datasource';
export * from './epm';
export * from './enrollment_api_key';
-export * from './fleet_setup';
export * from './install_script';
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
index 80d33d1b95b612..e75e5fe763d6ae 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
@@ -439,7 +439,7 @@ describe('xy_expression', () => {
});
test('onElementClick returns correct context data', () => {
- const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' };
+ const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null };
const series = {
key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
specId: 'd',
diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json
index 00c5e70ad6b8de..b8bad47327f223 100644
--- a/x-pack/plugins/maps/kibana.json
+++ b/x-pack/plugins/maps/kibana.json
@@ -5,12 +5,14 @@
"configPath": ["xpack", "maps"],
"requiredPlugins": [
"inspector",
+ "licensing",
"home",
"data",
"fileUpload",
"uiActions",
"navigation",
- "visualizations"
+ "visualizations",
+ "embeddable"
],
"ui": true
}
diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts
index 74bd305bff963f..bdcd14ea987825 100644
--- a/x-pack/plugins/maps/public/plugin.ts
+++ b/x-pack/plugins/maps/public/plugin.ts
@@ -39,11 +39,15 @@ import { getMapsVisTypeAlias } from './maps_vis_type_alias';
import { registerLayerWizards } from './layers/load_layer_wizards';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public';
+import { MAP_SAVED_OBJECT_TYPE } from '../common/constants';
+import { MapEmbeddableFactory } from './embeddable';
+import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public';
export interface MapsPluginSetupDependencies {
inspector: InspectorSetupContract;
home: HomePublicPluginSetup;
visualizations: VisualizationsSetup;
+ embeddable: EmbeddableSetup;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MapsPluginStartDependencies {}
@@ -102,12 +106,13 @@ export class MapsPlugin
MapsPluginStartDependencies
> {
public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) {
- const { inspector, home, visualizations } = plugins;
+ const { inspector, home, visualizations, embeddable } = plugins;
bindSetupCoreAndPlugins(core, plugins);
inspector.registerView(MapView);
home.featureCatalogue.register(featureCatalogueEntry);
visualizations.registerAlias(getMapsVisTypeAlias());
+ embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory());
}
public start(core: CoreStart, plugins: any) {
diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts
index 64d750f511f3a7..581770e59043f4 100644
--- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts
+++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts
@@ -5,7 +5,6 @@
*/
import { difference } from 'lodash';
-import Boom from 'boom';
import { IScopedClusterClient } from 'kibana/server';
import { EventManager, CalendarEvent } from './event_manager';
@@ -33,43 +32,31 @@ export class CalendarManager {
}
async getCalendar(calendarId: string) {
- try {
- const resp = await this._client('ml.calendars', {
- calendarId,
- });
-
- const calendars = resp.calendars;
- if (calendars.length) {
- const calendar = calendars[0];
- calendar.events = await this._eventManager.getCalendarEvents(calendarId);
- return calendar;
- } else {
- throw Boom.notFound(`Calendar with the id "${calendarId}" not found`);
- }
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ const resp = await this._client('ml.calendars', {
+ calendarId,
+ });
+
+ const calendars = resp.calendars;
+ const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found.
+ calendar.events = await this._eventManager.getCalendarEvents(calendarId);
+ return calendar;
}
async getAllCalendars() {
- try {
- const calendarsResp = await this._client('ml.calendars');
-
- const events: CalendarEvent[] = await this._eventManager.getAllEvents();
- const calendars: Calendar[] = calendarsResp.calendars;
- calendars.forEach(cal => (cal.events = []));
-
- // loop events and combine with related calendars
- events.forEach(event => {
- const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id);
- if (calendar) {
- calendar.events.push(event);
- }
- });
- return calendars;
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ const calendarsResp = await this._client('ml.calendars');
+
+ const events: CalendarEvent[] = await this._eventManager.getAllEvents();
+ const calendars: Calendar[] = calendarsResp.calendars;
+ calendars.forEach(cal => (cal.events = []));
+
+ // loop events and combine with related calendars
+ events.forEach(event => {
+ const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id);
+ if (calendar) {
+ calendar.events.push(event);
+ }
+ });
+ return calendars;
}
/**
@@ -78,12 +65,8 @@ export class CalendarManager {
* @returns {Promise<*>}
*/
async getCalendarsByIds(calendarIds: string) {
- try {
- const calendars: Calendar[] = await this.getAllCalendars();
- return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id));
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ const calendars: Calendar[] = await this.getAllCalendars();
+ return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id));
}
async newCalendar(calendar: FormCalendar) {
@@ -91,75 +74,67 @@ export class CalendarManager {
const events = calendar.events;
delete calendar.calendarId;
delete calendar.events;
- try {
- await this._client('ml.addCalendar', {
- calendarId,
- body: calendar,
- });
-
- if (events.length) {
- await this._eventManager.addEvents(calendarId, events);
- }
+ await this._client('ml.addCalendar', {
+ calendarId,
+ body: calendar,
+ });
- // return the newly created calendar
- return await this.getCalendar(calendarId);
- } catch (error) {
- throw Boom.badRequest(error);
+ if (events.length) {
+ await this._eventManager.addEvents(calendarId, events);
}
+
+ // return the newly created calendar
+ return await this.getCalendar(calendarId);
}
async updateCalendar(calendarId: string, calendar: Calendar) {
const origCalendar: Calendar = await this.getCalendar(calendarId);
- try {
- // update job_ids
- const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids);
- const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids);
-
- // workout the differences between the original events list and the new one
- // if an event has no event_id, it must be new
- const eventsToAdd = calendar.events.filter(
- event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined
- );
-
- // if an event in the original calendar cannot be found, it must have been deleted
- const eventsToRemove: CalendarEvent[] = origCalendar.events.filter(
- event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined
- );
-
- // note, both of the loops below could be removed if the add and delete endpoints
- // allowed multiple job_ids
-
- // add all new jobs
- if (jobsToAdd.length) {
- await this._client('ml.addJobToCalendar', {
- calendarId,
- jobId: jobsToAdd.join(','),
- });
- }
-
- // remove all removed jobs
- if (jobsToRemove.length) {
- await this._client('ml.removeJobFromCalendar', {
- calendarId,
- jobId: jobsToRemove.join(','),
- });
- }
+ // update job_ids
+ const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids);
+ const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids);
+
+ // workout the differences between the original events list and the new one
+ // if an event has no event_id, it must be new
+ const eventsToAdd = calendar.events.filter(
+ event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined
+ );
+
+ // if an event in the original calendar cannot be found, it must have been deleted
+ const eventsToRemove: CalendarEvent[] = origCalendar.events.filter(
+ event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined
+ );
+
+ // note, both of the loops below could be removed if the add and delete endpoints
+ // allowed multiple job_ids
+
+ // add all new jobs
+ if (jobsToAdd.length) {
+ await this._client('ml.addJobToCalendar', {
+ calendarId,
+ jobId: jobsToAdd.join(','),
+ });
+ }
- // add all new events
- if (eventsToAdd.length !== 0) {
- await this._eventManager.addEvents(calendarId, eventsToAdd);
- }
+ // remove all removed jobs
+ if (jobsToRemove.length) {
+ await this._client('ml.removeJobFromCalendar', {
+ calendarId,
+ jobId: jobsToRemove.join(','),
+ });
+ }
- // remove all removed events
- await Promise.all(
- eventsToRemove.map(async event => {
- await this._eventManager.deleteEvent(calendarId, event.event_id);
- })
- );
- } catch (error) {
- throw Boom.badRequest(error);
+ // add all new events
+ if (eventsToAdd.length !== 0) {
+ await this._eventManager.addEvents(calendarId, eventsToAdd);
}
+ // remove all removed events
+ await Promise.all(
+ eventsToRemove.map(async event => {
+ await this._eventManager.deleteEvent(calendarId, event.event_id);
+ })
+ );
+
// return the updated calendar
return await this.getCalendar(calendarId);
}
diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts
index 488839f68b3fe2..41240e2695f6fd 100644
--- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts
+++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts
@@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
export interface CalendarEvent {
@@ -23,41 +21,29 @@ export class EventManager {
}
async getCalendarEvents(calendarId: string) {
- try {
- const resp = await this._client('ml.events', { calendarId });
+ const resp = await this._client('ml.events', { calendarId });
- return resp.events;
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ return resp.events;
}
// jobId is optional
async getAllEvents(jobId?: string) {
const calendarId = GLOBAL_CALENDAR;
- try {
- const resp = await this._client('ml.events', {
- calendarId,
- jobId,
- });
+ const resp = await this._client('ml.events', {
+ calendarId,
+ jobId,
+ });
- return resp.events;
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ return resp.events;
}
async addEvents(calendarId: string, events: CalendarEvent[]) {
const body = { events };
- try {
- return await this._client('ml.addEvent', {
- calendarId,
- body,
- });
- } catch (error) {
- throw Boom.badRequest(error);
- }
+ return await this._client('ml.addEvent', {
+ calendarId,
+ body,
+ });
}
async deleteEvent(calendarId: string, eventId: string) {
diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
index a675eb58dc7929..ca63d69f403f6b 100644
--- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
+++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
@@ -138,12 +138,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
/**
* @apiGroup AnomalyDetectors
*
- * @api {put} /api/ml/anomaly_detectors/:jobId Instantiate an anomaly detection job
+ * @api {put} /api/ml/anomaly_detectors/:jobId Create an anomaly detection job
* @apiName CreateAnomalyDetectors
* @apiDescription Creates an anomaly detection job.
*
* @apiSchema (params) jobIdSchema
* @apiSchema (body) anomalyDetectionJobSchema
+ *
+ * @apiSuccess {Object} job the configuration of the job that has been created.
*/
router.put(
{
diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json
index 4848de6db7049c..555053089cb95a 100644
--- a/x-pack/plugins/ml/server/routes/apidoc.json
+++ b/x-pack/plugins/ml/server/routes/apidoc.json
@@ -49,7 +49,7 @@
"GetCategoryExamples",
"GetPartitionFieldsValues",
- "DataRecognizer",
+ "Modules",
"RecognizeIndex",
"GetModule",
"SetupModule",
diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts
index a4c0d5553a4b2b..20029fbd8d1a6b 100644
--- a/x-pack/plugins/ml/server/routes/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts
@@ -74,10 +74,12 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
*
* @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get stats for fields
* @apiName GetStatsForFields
- * @apiDescription Returns fields stats of the index pattern.
+ * @apiDescription Returns the stats on individual fields in the specified index pattern.
*
* @apiSchema (params) indexPatternTitleSchema
* @apiSchema (body) dataVisualizerFieldStatsSchema
+ *
+ * @apiSuccess {Object} fieldName stats by field, keyed on the name of the field.
*/
router.post(
{
@@ -130,10 +132,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
*
* @api {post} /api/ml/data_visualizer/get_overall_stats/:indexPatternTitle Get overall stats
* @apiName GetOverallStats
- * @apiDescription Returns overall stats of the index pattern.
+ * @apiDescription Returns the top level overall stats for the specified index pattern.
*
* @apiSchema (params) indexPatternTitleSchema
* @apiSchema (body) dataVisualizerOverallStatsSchema
+ *
+ * @apiSuccess {number} totalCount total count of documents.
+ * @apiSuccess {Object} aggregatableExistsFields stats on aggregatable fields that exist in documents.
+ * @apiSuccess {Object} aggregatableNotExistsFields stats on aggregatable fields that do not exist in documents.
+ * @apiSuccess {Object} nonAggregatableExistsFields stats on non-aggregatable fields that exist in documents.
+ * @apiSuccess {Object} nonAggregatableNotExistsFields stats on non-aggregatable fields that do not exist in documents.
*/
router.post(
{
diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts
index 9a5f47409c8a05..577e8e0161342a 100644
--- a/x-pack/plugins/ml/server/routes/fields_service.ts
+++ b/x-pack/plugins/ml/server/routes/fields_service.ts
@@ -37,6 +37,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) {
* @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field
*
* @apiSchema (body) getCardinalityOfFieldsSchema
+ *
+ * @apiSuccess {number} fieldName cardinality of the field.
*/
router.post(
{
@@ -64,9 +66,12 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) {
*
* @api {post} /api/ml/fields_service/time_field_range Get time field range
* @apiName GetTimeFieldRange
- * @apiDescription Returns the timefield range for the given index
+ * @apiDescription Returns the time range for the given index and query using the specified time range.
*
* @apiSchema (body) getTimeFieldRangeSchema
+ *
+ * @apiSuccess {Object} start start of time range with epoch and string properties.
+ * @apiSuccess {Object} end end of time range with epoch and string properties.
*/
router.post(
{
diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts
index 71499748691f68..1fe5a7af95d4f0 100644
--- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts
+++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts
@@ -7,7 +7,10 @@
import { wrapError } from '../client/error_wrapper';
import { RouteInitialization } from '../types';
import { jobAuditMessagesProvider } from '../models/job_audit_messages';
-import { jobAuditMessagesQuerySchema, jobIdSchema } from './schemas/job_audit_messages_schema';
+import {
+ jobAuditMessagesQuerySchema,
+ jobAuditMessagesJobIdSchema,
+} from './schemas/job_audit_messages_schema';
/**
* Routes for job audit message routes
@@ -20,14 +23,14 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio
* @apiName GetJobAuditMessages
* @apiDescription Returns audit messages for specified job ID
*
- * @apiSchema (params) jobIdSchema
+ * @apiSchema (params) jobAuditMessagesJobIdSchema
* @apiSchema (query) jobAuditMessagesQuerySchema
*/
router.get(
{
path: '/api/ml/job_audit_messages/messages/{jobId}',
validate: {
- params: jobIdSchema,
+ params: jobAuditMessagesJobIdSchema,
query: jobAuditMessagesQuerySchema,
},
},
diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts
index 493974cbafe366..cf973a914391c9 100644
--- a/x-pack/plugins/ml/server/routes/job_service.ts
+++ b/x-pack/plugins/ml/server/routes/job_service.ts
@@ -176,9 +176,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) {
*
* @api {post} /api/ml/jobs/jobs_summary Jobs summary
* @apiName JobsSummary
- * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars.
+ * @apiDescription Returns a list of anomaly detection jobs, with summary level information for every job.
+ * For any supplied job IDs, full job information will be returned, which include the analysis configuration,
+ * job stats, datafeed stats, and calendars.
*
* @apiSchema (body) jobIdsSchema
+ *
+ * @apiSuccess {Array} jobsList list of jobs. For any supplied job IDs, the job object will contain a fullJob property
+ * which includes the full configuration and stats for the job.
*/
router.post(
{
diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts
index 2d462b6dc207a8..2891144fc4574a 100644
--- a/x-pack/plugins/ml/server/routes/modules.ts
+++ b/x-pack/plugins/ml/server/routes/modules.ts
@@ -81,7 +81,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin
*/
export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
/**
- * @apiGroup DataRecognizer
+ * @apiGroup Modules
*
* @api {get} /api/ml/modules/recognize/:indexPatternTitle Recognize index pattern
* @apiName RecognizeIndex
@@ -111,7 +111,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
);
/**
- * @apiGroup DataRecognizer
+ * @apiGroup Modules
*
* @api {get} /api/ml/modules/get_module/:moduleId Get module
* @apiName GetModule
@@ -146,7 +146,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
);
/**
- * @apiGroup DataRecognizer
+ * @apiGroup Modules
*
* @api {post} /api/ml/modules/setup/:moduleId Setup module
* @apiName SetupModule
@@ -204,7 +204,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
);
/**
- * @apiGroup DataRecognizer
+ * @apiGroup Modules
*
* @api {post} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist
* @apiName CheckExistingModuleJobs
diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
index ab1305d9bc3548..9b86e3e06096e0 100644
--- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
@@ -119,7 +119,7 @@ export const anomalyDetectionJobSchema = {
};
export const jobIdSchema = schema.object({
- /** Job id */
+ /** Job ID. */
jobId: schema.string(),
});
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 1a1d02f991b558..b2d665954bd4dc 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
@@ -7,26 +7,41 @@
import { schema } from '@kbn/config-schema';
export const indexPatternTitleSchema = schema.object({
+ /** Title of the index pattern for which to return stats. */
indexPatternTitle: schema.string(),
});
export const dataVisualizerFieldStatsSchema = schema.object({
+ /** Query to match documents in the index. */
query: schema.any(),
fields: schema.arrayOf(schema.any()),
+ /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
+ /** Name of the time field in the index (optional). */
timeFieldName: schema.maybe(schema.string()),
+ /** Earliest timestamp for search, as epoch ms (optional). */
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()),
+ /** Maximum number of examples to return for text type fields. */
maxExamples: schema.number(),
});
export const dataVisualizerOverallStatsSchema = schema.object({
+ /** Query to match documents in the index. */
query: schema.any(),
+ /** Names of aggregatable fields for which to return stats. */
aggregatableFields: schema.arrayOf(schema.string()),
+ /** Names of non-aggregatable fields for which to return stats. */
nonAggregatableFields: schema.arrayOf(schema.string()),
+ /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
+ /** Name of the time field in the index (optional). */
timeFieldName: schema.maybe(schema.string()),
+ /** Earliest timestamp for search, as epoch ms (optional). */
earliest: schema.maybe(schema.number()),
+ /** Latest timestamp for search, as epoch ms (optional). */
latest: schema.maybe(schema.number()),
});
diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts
index e0fba498e0d58d..ba397e0084e270 100644
--- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts
@@ -7,16 +7,25 @@
import { schema } from '@kbn/config-schema';
export const getCardinalityOfFieldsSchema = schema.object({
+ /** Index or indexes for which to return the time range. */
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
+ /** Name(s) of the field(s) to return cardinality information. */
fieldNames: schema.maybe(schema.arrayOf(schema.string())),
+ /** Query to match documents in the index(es) (optional). */
query: schema.maybe(schema.any()),
+ /** Name of the time field in the index. */
timeFieldName: schema.maybe(schema.string()),
+ /** Earliest timestamp for search, as epoch ms (optional). */
earliestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])),
+ /** Latest timestamp for search, as epoch ms (optional). */
latestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])),
});
export const getTimeFieldRangeSchema = schema.object({
+ /** Index or indexes for which to return the time range. */
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
+ /** Name of the time field in the index. */
timeFieldName: schema.maybe(schema.string()),
+ /** Query to match documents in the index(es). */
query: schema.maybe(schema.any()),
});
diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts
index b94a004384eb18..ac489b3a6ce6f3 100644
--- a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts
@@ -6,7 +6,10 @@
import { schema } from '@kbn/config-schema';
-export const jobIdSchema = schema.object({ jobId: schema.maybe(schema.string()) });
+export const jobAuditMessagesJobIdSchema = schema.object({
+ /** Job ID. */
+ jobId: schema.maybe(schema.string()),
+});
export const jobAuditMessagesQuerySchema = schema.maybe(
schema.object({ from: schema.maybe(schema.any()) })
diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts
index d2036b8a7c0fa4..1ca1e5287e9d0f 100644
--- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts
@@ -40,6 +40,7 @@ export const forceStartDatafeedSchema = schema.object({
});
export const jobIdsSchema = schema.object({
+ /** Optional list of job ID(s). */
jobIds: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))])
),
diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts
index 979f7095cf933e..2e73b8cd044826 100644
--- a/x-pack/plugins/security/public/authentication/authentication_service.ts
+++ b/x-pack/plugins/security/public/authentication/authentication_service.ts
@@ -25,6 +25,11 @@ export interface AuthenticationServiceSetup {
* Returns currently authenticated user and throws if current user isn't authenticated.
*/
getCurrentUser: () => Promise;
+
+ /**
+ * Determines if API Keys are currently enabled.
+ */
+ areAPIKeysEnabled: () => Promise;
}
export class AuthenticationService {
@@ -37,11 +42,15 @@ export class AuthenticationService {
const getCurrentUser = async () =>
(await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser;
+ const areAPIKeysEnabled = async () =>
+ ((await http.get('/internal/security/api_key/_enabled')) as { apiKeysEnabled: boolean })
+ .apiKeysEnabled;
+
loginApp.create({ application, config, getStartServices, http });
logoutApp.create({ application, http });
loggedOutApp.create({ application, getStartServices, http });
overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices });
- return { getCurrentUser };
+ return { getCurrentUser, areAPIKeysEnabled };
}
}
diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts
index c8d77a5b62c6f2..dee0a24ab27c25 100644
--- a/x-pack/plugins/security/public/authentication/index.mock.ts
+++ b/x-pack/plugins/security/public/authentication/index.mock.ts
@@ -9,5 +9,6 @@ import { AuthenticationServiceSetup } from './authentication_service';
export const authenticationMock = {
createSetup: (): jest.Mocked => ({
getCurrentUser: jest.fn(),
+ areAPIKeysEnabled: jest.fn(),
}),
};
diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts
index 8e0ee73dfb613a..213c26d5287dcb 100644
--- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts
+++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts
@@ -10,7 +10,7 @@ import { AuthenticationServiceSetup } from '../authentication_service';
interface CreateDeps {
application: ApplicationSetup;
- authc: AuthenticationServiceSetup;
+ authc: Pick;
getStartServices: StartServicesAccessor;
}
diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx
index 1093957761d1c6..5b77266068ebf5 100644
--- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx
+++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx
@@ -14,7 +14,7 @@ import { AuthenticationStatePage } from '../components';
interface Props {
basePath: IBasePath;
- authc: AuthenticationServiceSetup;
+ authc: Pick;
}
export function OverwrittenSessionPage({ authc, basePath }: Props) {
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
index 372b1e56a73c47..a127379d972413 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts
@@ -10,6 +10,7 @@ import { ApiKey, ApiKeyToInvalidate } from '../../../common/model';
interface CheckPrivilegesResponse {
areApiKeysEnabled: boolean;
isAdmin: boolean;
+ canManage: boolean;
}
interface InvalidateApiKeysResponse {
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
index ae6ef4aa0fc34e..dea04a0eac3967 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
@@ -18,7 +18,6 @@ import { APIKeysGridPage } from './api_keys_grid_page';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { apiKeysAPIClientMock } from '../index.mock';
-const mock403 = () => ({ body: { statusCode: 403 } });
const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } });
const waitForRender = async (
@@ -48,6 +47,7 @@ describe('APIKeysGridPage', () => {
apiClientMock.checkPrivileges.mockResolvedValue({
isAdmin: true,
areApiKeysEnabled: true,
+ canManage: true,
});
apiClientMock.getApiKeys.mockResolvedValue({
apiKeys: [
@@ -82,6 +82,7 @@ describe('APIKeysGridPage', () => {
it('renders a callout when API keys are not enabled', async () => {
apiClientMock.checkPrivileges.mockResolvedValue({
isAdmin: true,
+ canManage: true,
areApiKeysEnabled: false,
});
@@ -95,7 +96,11 @@ describe('APIKeysGridPage', () => {
});
it('renders permission denied if user does not have required permissions', async () => {
- apiClientMock.checkPrivileges.mockRejectedValue(mock403());
+ apiClientMock.checkPrivileges.mockResolvedValue({
+ canManage: false,
+ isAdmin: false,
+ areApiKeysEnabled: true,
+ });
const wrapper = mountWithIntl();
@@ -152,6 +157,7 @@ describe('APIKeysGridPage', () => {
beforeEach(() => {
apiClientMock.checkPrivileges.mockResolvedValue({
isAdmin: false,
+ canManage: true,
areApiKeysEnabled: true,
});
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
index 698c0d37dbc645..9db09a34d3c3ff 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
@@ -26,7 +26,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment-timezone';
-import _ from 'lodash';
import { NotificationsStart } from 'src/core/public';
import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public';
import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model';
@@ -47,10 +46,10 @@ interface State {
isLoadingApp: boolean;
isLoadingTable: boolean;
isAdmin: boolean;
+ canManage: boolean;
areApiKeysEnabled: boolean;
apiKeys: ApiKey[];
selectedItems: ApiKey[];
- permissionDenied: boolean;
error: any;
}
@@ -63,9 +62,9 @@ export class APIKeysGridPage extends Component {
isLoadingApp: true,
isLoadingTable: false,
isAdmin: false,
+ canManage: false,
areApiKeysEnabled: false,
apiKeys: [],
- permissionDenied: false,
selectedItems: [],
error: undefined,
};
@@ -77,19 +76,15 @@ export class APIKeysGridPage extends Component {
public render() {
const {
- permissionDenied,
isLoadingApp,
isLoadingTable,
areApiKeysEnabled,
isAdmin,
+ canManage,
error,
apiKeys,
} = this.state;
- if (permissionDenied) {
- return ;
- }
-
if (isLoadingApp) {
return (
@@ -103,6 +98,10 @@ export class APIKeysGridPage extends Component {
);
}
+ if (!canManage) {
+ return ;
+ }
+
if (error) {
const {
body: { error: errorTitle, message, statusCode },
@@ -495,26 +494,25 @@ export class APIKeysGridPage extends Component {
private async checkPrivileges() {
try {
- const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges();
- this.setState({ isAdmin, areApiKeysEnabled });
+ const {
+ isAdmin,
+ canManage,
+ areApiKeysEnabled,
+ } = await this.props.apiKeysAPIClient.checkPrivileges();
+ this.setState({ isAdmin, canManage, areApiKeysEnabled });
- if (areApiKeysEnabled) {
- this.initiallyLoadApiKeys();
- } else {
- // We're done loading and will just show the "Disabled" error.
+ if (!canManage || !areApiKeysEnabled) {
this.setState({ isLoadingApp: false });
- }
- } catch (e) {
- if (_.get(e, 'body.statusCode') === 403) {
- this.setState({ permissionDenied: true, isLoadingApp: false });
} else {
- this.props.notifications.toasts.addDanger(
- i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', {
- defaultMessage: 'Error checking privileges: {message}',
- values: { message: _.get(e, 'body.message', '') },
- })
- );
+ this.initiallyLoadApiKeys();
}
+ } catch (e) {
+ this.props.notifications.toasts.addDanger(
+ i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', {
+ defaultMessage: 'Error checking privileges: {message}',
+ values: { message: e.body?.message ?? '' },
+ })
+ );
}
}
diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx
index 122b26378d22b0..7c57c4dd997a25 100644
--- a/x-pack/plugins/security/public/plugin.test.tsx
+++ b/x-pack/plugins/security/public/plugin.test.tsx
@@ -37,7 +37,7 @@ describe('Security Plugin', () => {
)
).toEqual({
__legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' },
- authc: { getCurrentUser: expect.any(Function) },
+ authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) },
license: {
isEnabled: expect.any(Function),
getFeatures: expect.any(Function),
@@ -63,7 +63,7 @@ describe('Security Plugin', () => {
expect(setupManagementServiceMock).toHaveBeenCalledTimes(1);
expect(setupManagementServiceMock).toHaveBeenCalledWith({
- authc: { getCurrentUser: expect.any(Function) },
+ authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) },
license: {
isEnabled: expect.any(Function),
getFeatures: expect.any(Function),
diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts
index 836740d0a547f4..9f2a628b575d57 100644
--- a/x-pack/plugins/security/server/authentication/api_keys.test.ts
+++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts
@@ -40,6 +40,82 @@ describe('API Keys', () => {
});
});
+ describe('areAPIKeysEnabled()', () => {
+ it('returns false when security feature is disabled', async () => {
+ mockLicense.isEnabled.mockReturnValue(false);
+
+ const result = await apiKeys.areAPIKeysEnabled();
+ expect(result).toEqual(false);
+ expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
+ expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled();
+ expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
+ });
+
+ it('returns false when the exception metadata indicates api keys are disabled', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ const error = new Error();
+ (error as any).body = {
+ error: { 'disabled.feature': 'api_keys' },
+ };
+ mockClusterClient.callAsInternalUser.mockRejectedValue(error);
+ const result = await apiKeys.areAPIKeysEnabled();
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(false);
+ });
+
+ it('returns true when the operation completes without error', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ mockClusterClient.callAsInternalUser.mockResolvedValue({});
+ const result = await apiKeys.areAPIKeysEnabled();
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(true);
+ });
+
+ it('throws the original error when exception metadata does not indicate that api keys are disabled', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ const error = new Error();
+ (error as any).body = {
+ error: { 'disabled.feature': 'something_else' },
+ };
+
+ mockClusterClient.callAsInternalUser.mockRejectedValue(error);
+ expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
+ });
+
+ it('throws the original error when exception metadata does not contain `disabled.feature`', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ const error = new Error();
+ (error as any).body = {};
+
+ mockClusterClient.callAsInternalUser.mockRejectedValue(error);
+ expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
+ });
+
+ it('throws the original error when exception contains no metadata', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ const error = new Error();
+
+ mockClusterClient.callAsInternalUser.mockRejectedValue(error);
+ expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls callCluster with proper parameters', async () => {
+ mockLicense.isEnabled.mockReturnValue(true);
+ mockClusterClient.callAsInternalUser.mockResolvedValueOnce({});
+
+ const result = await apiKeys.areAPIKeysEnabled();
+ expect(result).toEqual(true);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', {
+ body: {
+ id: 'kibana-api-key-service-test',
+ },
+ });
+ });
+ });
+
describe('create()', () => {
it('returns null when security feature is disabled', async () => {
mockLicense.isEnabled.mockReturnValue(false);
diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts
index 9df7219cec334e..29ff7e1f69f955 100644
--- a/x-pack/plugins/security/server/authentication/api_keys.ts
+++ b/x-pack/plugins/security/server/authentication/api_keys.ts
@@ -125,6 +125,35 @@ export class APIKeys {
this.license = license;
}
+ /**
+ * Determines if API Keys are enabled in Elasticsearch.
+ */
+ async areAPIKeysEnabled(): Promise {
+ if (!this.license.isEnabled()) {
+ return false;
+ }
+
+ const id = `kibana-api-key-service-test`;
+
+ this.logger.debug(
+ `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}`
+ );
+
+ try {
+ await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', {
+ body: {
+ id,
+ },
+ });
+ return true;
+ } catch (e) {
+ if (this.doesErrorIndicateAPIKeysAreDisabled(e)) {
+ return false;
+ }
+ throw e;
+ }
+ }
+
/**
* Tries to create an API key for the current user.
* @param request Request instance.
@@ -247,6 +276,11 @@ export class APIKeys {
return result;
}
+ private doesErrorIndicateAPIKeysAreDisabled(e: Record) {
+ const disabledFeature = e.body?.error?.['disabled.feature'];
+ return disabledFeature === 'api_keys';
+ }
+
private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams {
if (authorizationHeader.scheme.toLowerCase() === 'bearer') {
return {
diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
index 1c9b936692f9e8..9f1d6b27aa9d7a 100644
--- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
+++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks';
+import { httpServerMock } from 'src/core/server/mocks';
import { canRedirectRequest } from './can_redirect_request';
diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts
index 8092c1c81017b5..9397a7a42b3262 100644
--- a/x-pack/plugins/security/server/authentication/index.mock.ts
+++ b/x-pack/plugins/security/server/authentication/index.mock.ts
@@ -11,6 +11,7 @@ export const authenticationMock = {
login: jest.fn(),
logout: jest.fn(),
isProviderTypeEnabled: jest.fn(),
+ areAPIKeysEnabled: jest.fn(),
createAPIKey: jest.fn(),
getCurrentUser: jest.fn(),
grantAPIKeyAsInternalUser: jest.fn(),
diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts
index 5d7b49de68d286..d76a5a533d4983 100644
--- a/x-pack/plugins/security/server/authentication/index.ts
+++ b/x-pack/plugins/security/server/authentication/index.ts
@@ -172,6 +172,7 @@ export async function setupAuthentication({
getSessionInfo: authenticator.getSessionInfo.bind(authenticator),
isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator),
getCurrentUser,
+ areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(),
createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) =>
apiKeys.create(request, params),
grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request),
diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts
index a7a43a3031571a..ec50ac090f1e74 100644
--- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts
+++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts
@@ -315,117 +315,123 @@ describe('SAMLAuthenticationProvider', () => {
});
});
- it('redirects to the home page if new SAML Response is for the same user.', async () => {
- const request = httpServerMock.createKibanaRequest({ headers: {} });
- const state = {
- username: 'user',
- accessToken: 'existing-valid-token',
- refreshToken: 'existing-valid-refresh-token',
- realm: 'test-realm',
- };
- const authorization = `Bearer ${state.accessToken}`;
-
- const user = { username: 'user' };
- const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
- mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
- mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
-
- mockOptions.client.callAsInternalUser.mockResolvedValue({
- username: 'user',
- access_token: 'new-valid-token',
- refresh_token: 'new-valid-refresh-token',
- });
-
- mockOptions.tokens.invalidate.mockResolvedValue(undefined);
-
- await expect(
- provider.login(
- request,
- { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
- state
- )
- ).resolves.toEqual(
- AuthenticationResult.redirectTo('/base-path/', {
- state: {
- username: 'user',
- accessToken: 'new-valid-token',
- refreshToken: 'new-valid-refresh-token',
- realm: 'test-realm',
- },
- })
- );
-
- expectAuthenticateCall(mockOptions.client, { headers: { authorization } });
+ for (const [description, response] of [
+ ['session is valid', Promise.resolve({ username: 'user' })],
+ [
+ 'session is is expired',
+ Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())),
+ ],
+ ] as Array<[string, Promise]>) {
+ it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => {
+ const request = httpServerMock.createKibanaRequest({ headers: {} });
+ const state = {
+ username: 'user',
+ accessToken: 'existing-token',
+ refreshToken: 'existing-refresh-token',
+ realm: 'test-realm',
+ };
+ const authorization = `Bearer ${state.accessToken}`;
- expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.samlAuthenticate',
- {
- body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' },
- }
- );
+ const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
+ mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response);
+ mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
- expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
- expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
- accessToken: state.accessToken,
- refreshToken: state.refreshToken,
- });
- });
+ mockOptions.client.callAsInternalUser.mockResolvedValue({
+ username: 'user',
+ access_token: 'new-valid-token',
+ refresh_token: 'new-valid-refresh-token',
+ });
+
+ mockOptions.tokens.invalidate.mockResolvedValue(undefined);
+
+ await expect(
+ provider.login(
+ request,
+ { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
+ state
+ )
+ ).resolves.toEqual(
+ AuthenticationResult.redirectTo('/base-path/', {
+ state: {
+ username: 'user',
+ accessToken: 'new-valid-token',
+ refreshToken: 'new-valid-refresh-token',
+ realm: 'test-realm',
+ },
+ })
+ );
- it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => {
- const request = httpServerMock.createKibanaRequest({ headers: {} });
- const state = {
- username: 'user',
- accessToken: 'existing-valid-token',
- refreshToken: 'existing-valid-refresh-token',
- realm: 'test-realm',
- };
- const authorization = `Bearer ${state.accessToken}`;
+ expectAuthenticateCall(mockOptions.client, { headers: { authorization } });
- const existingUser = { username: 'user' };
- const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
- mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser);
- mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
+ expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.samlAuthenticate',
+ {
+ body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' },
+ }
+ );
- mockOptions.client.callAsInternalUser.mockResolvedValue({
- username: 'new-user',
- access_token: 'new-valid-token',
- refresh_token: 'new-valid-refresh-token',
+ expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
+ expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
+ accessToken: state.accessToken,
+ refreshToken: state.refreshToken,
+ });
});
- mockOptions.tokens.invalidate.mockResolvedValue(undefined);
+ it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => {
+ const request = httpServerMock.createKibanaRequest({ headers: {} });
+ const state = {
+ username: 'user',
+ accessToken: 'existing-token',
+ refreshToken: 'existing-refresh-token',
+ realm: 'test-realm',
+ };
+ const authorization = `Bearer ${state.accessToken}`;
- await expect(
- provider.login(
- request,
- { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
- state
- )
- ).resolves.toEqual(
- AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
- state: {
- username: 'new-user',
- accessToken: 'new-valid-token',
- refreshToken: 'new-valid-refresh-token',
- realm: 'test-realm',
- },
- })
- );
+ const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
+ mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response);
+ mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
+
+ mockOptions.client.callAsInternalUser.mockResolvedValue({
+ username: 'new-user',
+ access_token: 'new-valid-token',
+ refresh_token: 'new-valid-refresh-token',
+ });
+
+ mockOptions.tokens.invalidate.mockResolvedValue(undefined);
+
+ await expect(
+ provider.login(
+ request,
+ { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
+ state
+ )
+ ).resolves.toEqual(
+ AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
+ state: {
+ username: 'new-user',
+ accessToken: 'new-valid-token',
+ refreshToken: 'new-valid-refresh-token',
+ realm: 'test-realm',
+ },
+ })
+ );
- expectAuthenticateCall(mockOptions.client, { headers: { authorization } });
+ expectAuthenticateCall(mockOptions.client, { headers: { authorization } });
- expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.samlAuthenticate',
- {
- body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' },
- }
- );
+ expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.samlAuthenticate',
+ {
+ body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' },
+ }
+ );
- expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
- expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
- accessToken: state.accessToken,
- refreshToken: state.refreshToken,
+ expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
+ expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
+ accessToken: state.accessToken,
+ refreshToken: state.refreshToken,
+ });
});
- });
+ }
});
describe('User initiated login with captured redirect URL', () => {
diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts
index e14d34d1901eb7..5c5ec498909016 100644
--- a/x-pack/plugins/security/server/authentication/providers/saml.ts
+++ b/x-pack/plugins/security/server/authentication/providers/saml.ts
@@ -158,10 +158,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
return await this.loginWithSAMLResponse(request, samlResponse, state);
}
- if (authenticationResult.succeeded()) {
- // If user has been authenticated via session, but request also includes SAML payload
- // we should check whether this payload is for the exactly same user and if not
- // we'll re-authenticate user and forward to a page with the respective warning.
+ // If user has been authenticated via session or failed to do so because of expired access token,
+ // but request also includes SAML payload we should check whether this payload is for the exactly
+ // same user and if not we'll re-authenticate user and forward to a page with the respective warning.
+ if (
+ authenticationResult.succeeded() ||
+ (authenticationResult.failed() &&
+ Tokens.isAccessTokenExpiredError(authenticationResult.error))
+ ) {
return await this.loginWithNewSAMLResponse(
request,
samlResponse,
diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts
index 82f29310c04c02..57366183050d7e 100644
--- a/x-pack/plugins/security/server/authentication/tokens.test.ts
+++ b/x-pack/plugins/security/server/authentication/tokens.test.ts
@@ -25,7 +25,7 @@ describe('Tokens', () => {
tokens = new Tokens(tokensOptions);
});
- it('isAccessTokenExpiredError() returns `true` only if token expired or its document is missing', () => {
+ it('isAccessTokenExpiredError() returns `true` only if token expired', () => {
const nonExpirationErrors = [
{},
new Error(),
@@ -91,55 +91,66 @@ describe('Tokens', () => {
});
describe('invalidate()', () => {
- it('throws if call to delete access token responds with an error', async () => {
- const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
-
- const failureReason = new Error('failed to delete token');
- mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => {
- if (args && args.body && args.body.token) {
- return Promise.reject(failureReason);
- }
-
- return Promise.resolve({ invalidated_tokens: 1 });
+ for (const [description, failureReason] of [
+ ['an unknown error', new Error('failed to delete token')],
+ ['a 404 error without body', { statusCode: 404 }],
+ ] as Array<[string, object]>) {
+ it(`throws if call to delete access token responds with ${description}`, async () => {
+ const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
+
+ mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => {
+ if (args && args.body && args.body.token) {
+ return Promise.reject(failureReason);
+ }
+
+ return Promise.resolve({ invalidated_tokens: 1 });
+ });
+
+ await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason);
+
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { token: tokenPair.accessToken },
+ }
+ );
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { refresh_token: tokenPair.refreshToken },
+ }
+ );
});
- await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason);
-
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { token: tokenPair.accessToken } }
- );
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { refresh_token: tokenPair.refreshToken } }
- );
- });
-
- it('throws if call to delete refresh token responds with an error', async () => {
- const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
-
- const failureReason = new Error('failed to delete token');
- mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => {
- if (args && args.body && args.body.refresh_token) {
- return Promise.reject(failureReason);
- }
-
- return Promise.resolve({ invalidated_tokens: 1 });
+ it(`throws if call to delete refresh token responds with ${description}`, async () => {
+ const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
+
+ mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => {
+ if (args && args.body && args.body.refresh_token) {
+ return Promise.reject(failureReason);
+ }
+
+ return Promise.resolve({ invalidated_tokens: 1 });
+ });
+
+ await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason);
+
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { token: tokenPair.accessToken },
+ }
+ );
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { refresh_token: tokenPair.refreshToken },
+ }
+ );
});
-
- await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason);
-
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { token: tokenPair.accessToken } }
- );
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { refresh_token: tokenPair.refreshToken } }
- );
- });
+ }
it('invalidates all provided tokens', async () => {
const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
@@ -187,23 +198,35 @@ describe('Tokens', () => {
);
});
- it('does not fail if none of the tokens were invalidated', async () => {
- const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
-
- mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 0 });
-
- await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined);
-
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { token: tokenPair.accessToken } }
- );
- expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
- 'shield.deleteAccessToken',
- { body: { refresh_token: tokenPair.refreshToken } }
- );
- });
+ for (const [description, response] of [
+ ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })],
+ [
+ '404 error is returned',
+ Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }),
+ ],
+ ] as Array<[string, Promise]>) {
+ it(`does not fail if ${description}`, async () => {
+ const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
+
+ mockClusterClient.callAsInternalUser.mockImplementation(() => response);
+
+ await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined);
+
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2);
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { token: tokenPair.accessToken },
+ }
+ );
+ expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.deleteAccessToken',
+ {
+ body: { refresh_token: tokenPair.refreshToken },
+ }
+ );
+ });
+ }
it('does not fail if more than one token per access or refresh token were invalidated', async () => {
const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts
index ea7b5d5a9ff38a..9117c9a679a4a2 100644
--- a/x-pack/plugins/security/server/authentication/tokens.ts
+++ b/x-pack/plugins/security/server/authentication/tokens.ts
@@ -103,8 +103,15 @@ export class Tokens {
).invalidated_tokens;
} catch (err) {
this.logger.debug(`Failed to invalidate refresh token: ${err.message}`);
- // We don't re-throw the error here to have a chance to invalidate access token if it's provided.
- invalidationError = err;
+
+ // When using already deleted refresh token, Elasticsearch responds with 404 and a body that
+ // shows that no tokens were invalidated.
+ if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) {
+ invalidatedTokensCount = err.body.invalidated_tokens;
+ } else {
+ // We don't re-throw the error here to have a chance to invalidate access token if it's provided.
+ invalidationError = err;
+ }
}
if (invalidatedTokensCount === 0) {
@@ -128,7 +135,14 @@ export class Tokens {
).invalidated_tokens;
} catch (err) {
this.logger.debug(`Failed to invalidate access token: ${err.message}`);
- invalidationError = err;
+
+ // When using already deleted access token, Elasticsearch responds with 404 and a body that
+ // shows that no tokens were invalidated.
+ if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) {
+ invalidatedTokensCount = err.body.invalidated_tokens;
+ } else {
+ invalidationError = err;
+ }
}
if (invalidatedTokensCount === 0) {
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 4767f57de764c2..3ce0198273af97 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -69,6 +69,7 @@ describe('Security Plugin', () => {
"registerPrivilegesWithCluster": [Function],
},
"authc": Object {
+ "areAPIKeysEnabled": [Function],
"createAPIKey": [Function],
"getCurrentUser": [Function],
"getSessionInfo": [Function],
diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts
new file mode 100644
index 00000000000000..3c6dc3c0d7bdaa
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server';
+import { LicenseCheck } from '../../../../licensing/server';
+
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { routeDefinitionParamsMock } from '../index.mock';
+import Boom from 'boom';
+import { defineEnabledApiKeysRoutes } from './enabled';
+import { APIKeys } from '../../authentication/api_keys';
+
+interface TestOptions {
+ licenseCheckResult?: LicenseCheck;
+ apiResponse?: () => Promise;
+ asserts: { statusCode: number; result?: Record };
+}
+
+describe('API keys enabled', () => {
+ const enabledApiKeysTest = (
+ description: string,
+ { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions
+ ) => {
+ test(description, async () => {
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+
+ const apiKeys = new APIKeys({
+ logger: mockRouteDefinitionParams.logger,
+ clusterClient: mockRouteDefinitionParams.clusterClient,
+ license: mockRouteDefinitionParams.license,
+ });
+
+ mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() =>
+ apiKeys.areAPIKeysEnabled()
+ );
+
+ if (apiResponse) {
+ mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse);
+ }
+
+ defineEnabledApiKeysRoutes(mockRouteDefinitionParams);
+ const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
+
+ const headers = { authorization: 'foo' };
+ const mockRequest = httpServerMock.createKibanaRequest({
+ method: 'get',
+ path: '/internal/security/api_key/_enabled',
+ headers,
+ });
+ const mockContext = ({
+ licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } },
+ } as unknown) as RequestHandlerContext;
+
+ const response = await handler(mockContext, mockRequest, kibanaResponseFactory);
+ expect(response.status).toBe(asserts.statusCode);
+ expect(response.payload).toEqual(asserts.result);
+
+ if (apiResponse) {
+ expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ 'shield.invalidateAPIKey',
+ {
+ body: {
+ id: expect.any(String),
+ },
+ }
+ );
+ } else {
+ expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled();
+ }
+ expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic');
+ });
+ };
+
+ describe('failure', () => {
+ enabledApiKeysTest('returns result of license checker', {
+ licenseCheckResult: { state: 'invalid', message: 'test forbidden message' },
+ asserts: { statusCode: 403, result: { message: 'test forbidden message' } },
+ });
+
+ const error = Boom.notAcceptable('test not acceptable message');
+ enabledApiKeysTest('returns error from cluster client', {
+ apiResponse: async () => {
+ throw error;
+ },
+ asserts: { statusCode: 406, result: error },
+ });
+ });
+
+ describe('success', () => {
+ enabledApiKeysTest('returns true if API Keys are enabled', {
+ apiResponse: async () => ({}),
+ asserts: {
+ statusCode: 200,
+ result: {
+ apiKeysEnabled: true,
+ },
+ },
+ });
+ enabledApiKeysTest('returns false if API Keys are disabled', {
+ apiResponse: async () => {
+ const error = new Error();
+ (error as any).body = {
+ error: { 'disabled.feature': 'api_keys' },
+ };
+ throw error;
+ },
+ asserts: {
+ statusCode: 200,
+ result: {
+ apiKeysEnabled: false,
+ },
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts
new file mode 100644
index 00000000000000..2f5b8343bcd89a
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api_keys/enabled.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 { wrapIntoCustomErrorResponse } from '../../errors';
+import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { RouteDefinitionParams } from '..';
+
+export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) {
+ router.get(
+ {
+ path: '/internal/security/api_key/_enabled',
+ validate: false,
+ },
+ createLicensedRouteHandler(async (context, request, response) => {
+ try {
+ const apiKeysEnabled = await authc.areAPIKeysEnabled();
+
+ return response.ok({ body: { apiKeysEnabled } });
+ } catch (error) {
+ return response.customError(wrapIntoCustomErrorResponse(error));
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts
index d75eb1bcbe9614..7ac37bbead6136 100644
--- a/x-pack/plugins/security/server/routes/api_keys/index.ts
+++ b/x-pack/plugins/security/server/routes/api_keys/index.ts
@@ -7,9 +7,11 @@
import { defineGetApiKeysRoutes } from './get';
import { defineCheckPrivilegesRoutes } from './privileges';
import { defineInvalidateApiKeysRoutes } from './invalidate';
+import { defineEnabledApiKeysRoutes } from './enabled';
import { RouteDefinitionParams } from '..';
export function defineApiKeysRoutes(params: RouteDefinitionParams) {
+ defineEnabledApiKeysRoutes(params);
defineGetApiKeysRoutes(params);
defineCheckPrivilegesRoutes(params);
defineInvalidateApiKeysRoutes(params);
diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts
index 311d50e9eb1691..afb67dc3bbfca1 100644
--- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts
+++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts
@@ -11,25 +11,53 @@ import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../
import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { routeDefinitionParamsMock } from '../index.mock';
import { defineCheckPrivilegesRoutes } from './privileges';
+import { APIKeys } from '../../authentication/api_keys';
interface TestOptions {
licenseCheckResult?: LicenseCheck;
- apiResponses?: Array<() => Promise>;
- asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] };
+ callAsInternalUserResponses?: Array<() => Promise>;
+ callAsCurrentUserResponses?: Array<() => Promise>;
+ asserts: {
+ statusCode: number;
+ result?: Record;
+ callAsInternalUserAPIArguments?: unknown[][];
+ callAsCurrentUserAPIArguments?: unknown[][];
+ };
}
describe('Check API keys privileges', () => {
const getPrivilegesTest = (
description: string,
- { licenseCheckResult = { state: 'valid' }, apiResponses = [], asserts }: TestOptions
+ {
+ licenseCheckResult = { state: 'valid' },
+ callAsInternalUserResponses = [],
+ callAsCurrentUserResponses = [],
+ asserts,
+ }: TestOptions
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+
+ const apiKeys = new APIKeys({
+ logger: mockRouteDefinitionParams.logger,
+ clusterClient: mockRouteDefinitionParams.clusterClient,
+ license: mockRouteDefinitionParams.license,
+ });
+
+ mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() =>
+ apiKeys.areAPIKeysEnabled()
+ );
+
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient);
- for (const apiResponse of apiResponses) {
+ for (const apiResponse of callAsCurrentUserResponses) {
mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse);
}
+ for (const apiResponse of callAsInternalUserResponses) {
+ mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce(
+ apiResponse
+ );
+ }
defineCheckPrivilegesRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
@@ -48,8 +76,8 @@ describe('Check API keys privileges', () => {
expect(response.status).toBe(asserts.statusCode);
expect(response.payload).toEqual(asserts.result);
- if (Array.isArray(asserts.apiArguments)) {
- for (const apiArguments of asserts.apiArguments) {
+ if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) {
+ for (const apiArguments of asserts.callAsCurrentUserAPIArguments) {
expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(
mockRequest
);
@@ -58,6 +86,17 @@ describe('Check API keys privileges', () => {
} else {
expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
}
+
+ if (Array.isArray(asserts.callAsInternalUserAPIArguments)) {
+ for (const apiArguments of asserts.callAsInternalUserAPIArguments) {
+ expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith(
+ ...apiArguments
+ );
+ }
+ } else {
+ expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled();
+ }
+
expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic');
});
};
@@ -70,16 +109,21 @@ describe('Check API keys privileges', () => {
const error = Boom.notAcceptable('test not acceptable message');
getPrivilegesTest('returns error from cluster client', {
- apiResponses: [
+ callAsCurrentUserResponses: [
async () => {
throw error;
},
- async () => {},
],
+ callAsInternalUserResponses: [async () => {}],
asserts: {
- apiArguments: [
- ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }],
- ['shield.getAPIKeys', { owner: true }],
+ callAsCurrentUserAPIArguments: [
+ [
+ 'shield.hasPrivileges',
+ { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } },
+ ],
+ ],
+ callAsInternalUserAPIArguments: [
+ ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }],
],
statusCode: 406,
result: error,
@@ -89,14 +133,16 @@ describe('Check API keys privileges', () => {
describe('success', () => {
getPrivilegesTest('returns areApiKeysEnabled and isAdmin', {
- apiResponses: [
+ callAsCurrentUserResponses: [
async () => ({
username: 'elastic',
has_all_requested: true,
- cluster: { manage_api_key: true, manage_security: true },
+ cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false },
index: {},
application: {},
}),
+ ],
+ callAsInternalUserResponses: [
async () => ({
api_keys: [
{
@@ -112,71 +158,108 @@ describe('Check API keys privileges', () => {
}),
],
asserts: {
- apiArguments: [
- ['shield.getAPIKeys', { owner: true }],
- ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }],
+ callAsCurrentUserAPIArguments: [
+ [
+ 'shield.hasPrivileges',
+ { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } },
+ ],
+ ],
+ callAsInternalUserAPIArguments: [
+ ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }],
],
statusCode: 200,
- result: { areApiKeysEnabled: true, isAdmin: true },
+ result: { areApiKeysEnabled: true, isAdmin: true, canManage: true },
},
});
getPrivilegesTest(
- 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"',
+ 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch',
{
- apiResponses: [
+ callAsCurrentUserResponses: [
async () => ({
username: 'elastic',
has_all_requested: true,
- cluster: { manage_api_key: true, manage_security: true },
+ cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true },
index: {},
application: {},
}),
+ ],
+ callAsInternalUserResponses: [
async () => {
- throw Boom.unauthorized('api keys are not enabled');
+ const error = new Error();
+ (error as any).body = {
+ error: {
+ 'disabled.feature': 'api_keys',
+ },
+ };
+ throw error;
},
],
asserts: {
- apiArguments: [
- ['shield.getAPIKeys', { owner: true }],
- ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }],
+ callAsCurrentUserAPIArguments: [
+ [
+ 'shield.hasPrivileges',
+ { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } },
+ ],
+ ],
+ callAsInternalUserAPIArguments: [
+ ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }],
],
statusCode: 200,
- result: { areApiKeysEnabled: false, isAdmin: true },
+ result: { areApiKeysEnabled: false, isAdmin: true, canManage: true },
},
}
);
getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', {
- apiResponses: [
+ callAsCurrentUserResponses: [
async () => ({
username: 'elastic',
has_all_requested: true,
- cluster: { manage_api_key: false, manage_security: false },
+ cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false },
index: {},
application: {},
}),
- async () => ({
- api_keys: [
- {
- id: 'si8If24B1bKsmSLTAhJV',
- name: 'my-api-key',
- creation: 1574089261632,
- expiration: 1574175661632,
- invalidated: false,
- username: 'elastic',
- realm: 'reserved',
- },
+ ],
+ callAsInternalUserResponses: [async () => ({})],
+ asserts: {
+ callAsCurrentUserAPIArguments: [
+ [
+ 'shield.hasPrivileges',
+ { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } },
],
+ ],
+ callAsInternalUserAPIArguments: [
+ ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }],
+ ],
+ statusCode: 200,
+ result: { areApiKeysEnabled: true, isAdmin: false, canManage: false },
+ },
+ });
+
+ getPrivilegesTest('returns canManage=true when user can manage their own API Keys', {
+ callAsCurrentUserResponses: [
+ async () => ({
+ username: 'elastic',
+ has_all_requested: true,
+ cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true },
+ index: {},
+ application: {},
}),
],
+ callAsInternalUserResponses: [async () => ({})],
asserts: {
- apiArguments: [
- ['shield.getAPIKeys', { owner: true }],
- ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }],
+ callAsCurrentUserAPIArguments: [
+ [
+ 'shield.hasPrivileges',
+ { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } },
+ ],
+ ],
+ callAsInternalUserAPIArguments: [
+ ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }],
],
statusCode: 200,
- result: { areApiKeysEnabled: true, isAdmin: false },
+ result: { areApiKeysEnabled: true, isAdmin: false, canManage: true },
},
});
});
diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts
index 216d1ef1bf4a44..9cccb967527729 100644
--- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts
+++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts
@@ -8,7 +8,11 @@ import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
import { RouteDefinitionParams } from '..';
-export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) {
+export function defineCheckPrivilegesRoutes({
+ router,
+ clusterClient,
+ authc,
+}: RouteDefinitionParams) {
router.get(
{
path: '/internal/security/api_key/privileges',
@@ -20,26 +24,25 @@ export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefi
const [
{
- cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey },
+ cluster: {
+ manage_security: manageSecurity,
+ manage_api_key: manageApiKey,
+ manage_own_api_key: manageOwnApiKey,
+ },
},
- { areApiKeysEnabled },
+ areApiKeysEnabled,
] = await Promise.all([
scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', {
- body: { cluster: ['manage_security', 'manage_api_key'] },
+ body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] },
}),
- scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then(
- // If the API returns a truthy result that means it's enabled.
- result => ({ areApiKeysEnabled: !!result }),
- // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759.
- e =>
- e.message.includes('api keys are not enabled')
- ? Promise.resolve({ areApiKeysEnabled: false })
- : Promise.reject(e)
- ),
+ authc.areAPIKeysEnabled(),
]);
+ const isAdmin = manageSecurity || manageApiKey;
+ const canManage = manageSecurity || manageApiKey || manageOwnApiKey;
+
return response.ok({
- body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey },
+ body: { areApiKeysEnabled, isAdmin, canManage },
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts
index 6244a4cc64e683..e8d778bddadc2b 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { loggerMock } from 'src/core/server/logging/logger.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
import { getResult } from '../routes/__mocks__/request_responses';
import { rulesNotificationAlertType } from './rules_notification_alert_type';
import { buildSignalsSearchQuery } from './build_signals_query';
@@ -15,12 +15,12 @@ jest.mock('./build_signals_query');
describe('rules_notification_alert_type', () => {
let payload: NotificationExecutorOptions;
let alert: ReturnType;
- let logger: ReturnType;
+ let logger: ReturnType;
let alertServices: AlertServicesMock;
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
- logger = loggerMock.create();
+ logger = loggingServiceMock.createLogger();
payload = {
alertId: '1111',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts
index 4fce037b483d5a..0c9ccf069b3b62 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { loggerMock } from 'src/core/server/logging/logger.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
import { getNotificationResult, getResult } from '../routes/__mocks__/request_responses';
import { isAlertTypes, isNotificationAlertExecutor } from './types';
import { rulesNotificationAlertType } from './rules_notification_alert_type';
@@ -20,7 +20,9 @@ describe('types', () => {
it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => {
expect(
- isNotificationAlertExecutor(rulesNotificationAlertType({ logger: loggerMock.create() }))
+ isNotificationAlertExecutor(
+ rulesNotificationAlertType({ logger: loggingServiceMock.createLogger() })
+ )
).toEqual(true);
});
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 7eecc5cb9bad03..0c7f0839f8daf5 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -5,7 +5,7 @@
*/
import moment from 'moment';
-import { loggerMock } from 'src/core/server/logging/logger.mock';
+import { loggingServiceMock } from 'src/core/server/mocks';
import { getResult, getMlResult } from '../routes/__mocks__/request_responses';
import { signalRulesAlertType } from './signal_rule_alert_type';
import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
@@ -70,13 +70,13 @@ describe('rules_notification_alert_type', () => {
};
let payload: jest.Mocked;
let alert: ReturnType;
- let logger: ReturnType;
+ let logger: ReturnType;
let alertServices: AlertServicesMock;
let ruleStatusService: Record;
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
- logger = loggerMock.create();
+ logger = loggingServiceMock.createLogger();
ruleStatusService = {
success: jest.fn(),
find: jest.fn(),
diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts
index 866dbe9480ca59..8a1e4271303a6c 100644
--- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts
+++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts
@@ -6,7 +6,7 @@
import { buildRouteValidation } from './route_validation';
import * as rt from 'io-ts';
-import { RouteValidationResultFactory } from '../../../../../../src/core/server/http';
+import { RouteValidationResultFactory } from 'src/core/server';
describe('buildRouteValidation', () => {
const schema = rt.exact(
diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts
index fb87b6290a3dab..dee4d80b979172 100644
--- a/x-pack/plugins/task_manager/server/task_pool.test.ts
+++ b/x-pack/plugins/task_manager/server/task_pool.test.ts
@@ -9,6 +9,7 @@ import { TaskPool, TaskPoolRunResult } from './task_pool';
import { mockLogger, resolvable, sleep } from './test_utils';
import { asOk } from './lib/result_type';
import { SavedObjectsErrorHelpers } from '../../../../src/core/server';
+import moment from 'moment';
describe('TaskPool', () => {
test('occupiedWorkers are a sum of running tasks', async () => {
@@ -190,14 +191,16 @@ describe('TaskPool', () => {
});
test('run cancels expired tasks prior to running new tasks', async () => {
+ const logger = mockLogger();
const pool = new TaskPool({
maxWorkers: 2,
- logger: mockLogger(),
+ logger,
});
const expired = resolvable();
const shouldRun = sinon.spy(() => Promise.resolve());
const shouldNotRun = sinon.spy(() => Promise.resolve());
+ const now = new Date();
const result = await pool.run([
{
...mockTask(),
@@ -207,6 +210,16 @@ describe('TaskPool', () => {
await sleep(10);
return asOk({ state: {} });
},
+ get expiration() {
+ return now;
+ },
+ get startedAt() {
+ // 5 and a half minutes
+ return moment(now)
+ .subtract(5, 'm')
+ .subtract(30, 's')
+ .toDate();
+ },
cancel: shouldRun,
},
{
@@ -231,6 +244,10 @@ describe('TaskPool', () => {
expect(pool.occupiedWorkers).toEqual(2);
expect(pool.availableWorkers).toEqual(0);
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).`
+ );
});
test('logs if cancellation errors', async () => {
@@ -285,6 +302,20 @@ describe('TaskPool', () => {
markTaskAsRunning: jest.fn(async () => true),
run: mockRun(),
toString: () => `TaskType "shooooo"`,
+ get expiration() {
+ return new Date();
+ },
+ get startedAt() {
+ return new Date();
+ },
+ get definition() {
+ return {
+ type: '',
+ title: '',
+ timeout: '5m',
+ createTaskRunner: jest.fn(),
+ };
+ },
};
}
});
diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts
index 8999fb48680ce6..bd0de86551aaaa 100644
--- a/x-pack/plugins/task_manager/server/task_pool.ts
+++ b/x-pack/plugins/task_manager/server/task_pool.ts
@@ -8,7 +8,9 @@
* This module contains the logic that ensures we don't run too many
* tasks at once in a given Kibana instance.
*/
+import moment, { Duration } from 'moment';
import { performance } from 'perf_hooks';
+import { padLeft } from 'lodash';
import { Logger } from './types';
import { TaskRunner } from './task_runner';
import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error';
@@ -148,7 +150,19 @@ export class TaskPool {
private cancelExpiredTasks() {
for (const task of this.running) {
if (task.isExpired) {
- this.logger.debug(`Cancelling expired task ${task.toString()}.`);
+ this.logger.warn(
+ `Cancelling task ${task.toString()} as it expired at ${task.expiration.toISOString()}${
+ task.startedAt
+ ? ` after running for ${durationAsString(
+ moment.duration(
+ moment(new Date())
+ .utc()
+ .diff(task.startedAt)
+ )
+ )}`
+ : ``
+ }${task.definition.timeout ? ` (with timeout set at ${task.definition.timeout})` : ``}.`
+ );
this.cancelTask(task);
}
}
@@ -169,3 +183,8 @@ function partitionListByCount(list: T[], count: number): [T[], T[]] {
const listInCount = list.splice(0, count);
return [listInCount, list];
}
+
+function durationAsString(duration: Duration): string {
+ const [m, s] = [duration.minutes(), duration.seconds()].map(value => padLeft(`${value}`, 2, '0'));
+ return `${m}m ${s}s`;
+}
diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts
index 3f0132105347e7..fad3bf96905ae6 100644
--- a/x-pack/plugins/task_manager/server/task_runner.test.ts
+++ b/x-pack/plugins/task_manager/server/task_runner.test.ts
@@ -13,6 +13,7 @@ import { ConcreteTaskInstance, TaskStatus } from './task';
import { TaskManagerRunner } from './task_runner';
import { mockLogger } from './test_utils';
import { SavedObjectsErrorHelpers } from '../../../../src/core/server';
+import moment from 'moment';
let fakeTimer: sinon.SinonFakeTimers;
@@ -113,6 +114,60 @@ describe('TaskManagerRunner', () => {
expect(instance.runAt.getTime()).toBeLessThanOrEqual(minutesFromNow(10).getTime());
});
+ test('expiration returns time after which timeout will have elapsed from start', async () => {
+ const now = moment();
+ const { runner } = testOpts({
+ instance: {
+ schedule: { interval: '10m' },
+ status: TaskStatus.Running,
+ startedAt: now.toDate(),
+ },
+ definitions: {
+ bar: {
+ timeout: `1m`,
+ createTaskRunner: () => ({
+ async run() {
+ return;
+ },
+ }),
+ },
+ },
+ });
+
+ await runner.run();
+
+ expect(runner.isExpired).toBe(false);
+ expect(runner.expiration).toEqual(now.add(1, 'm').toDate());
+ });
+
+ test('runDuration returns duration which has elapsed since start', async () => {
+ const now = moment()
+ .subtract(30, 's')
+ .toDate();
+ const { runner } = testOpts({
+ instance: {
+ schedule: { interval: '10m' },
+ status: TaskStatus.Running,
+ startedAt: now,
+ },
+ definitions: {
+ bar: {
+ timeout: `1m`,
+ createTaskRunner: () => ({
+ async run() {
+ return;
+ },
+ }),
+ },
+ },
+ });
+
+ await runner.run();
+
+ expect(runner.isExpired).toBe(false);
+ expect(runner.startedAt).toEqual(now);
+ });
+
test('reschedules tasks that return a runAt', async () => {
const runAt = minutesFromNow(_.random(1, 10));
const { runner, store } = testOpts({
@@ -208,7 +263,7 @@ describe('TaskManagerRunner', () => {
expect(logger.warn).not.toHaveBeenCalled();
});
- test('warns if cancel is called on a non-cancellable task', async () => {
+ test('debug logs if cancel is called on a non-cancellable task', async () => {
const { runner, logger } = testOpts({
definitions: {
bar: {
@@ -223,10 +278,7 @@ describe('TaskManagerRunner', () => {
await runner.cancel();
await promise;
- expect(logger.warn).toHaveBeenCalledTimes(1);
- expect(logger.warn.mock.calls[0][0]).toMatchInlineSnapshot(
- `"The task bar \\"foo\\" is not cancellable."`
- );
+ expect(logger.debug).toHaveBeenCalledWith(`The task bar "foo" is not cancellable.`);
});
test('sets startedAt, status, attempts and retryAt when claiming a task', async () => {
diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts
index 682885aaa0b1ca..ec1c40dc807314 100644
--- a/x-pack/plugins/task_manager/server/task_runner.ts
+++ b/x-pack/plugins/task_manager/server/task_runner.ts
@@ -39,6 +39,9 @@ const EMPTY_RUN_RESULT: SuccessfulRunResult = {};
export interface TaskRunner {
isExpired: boolean;
+ expiration: Date;
+ startedAt: Date | null;
+ definition: TaskDefinition;
cancel: CancelFunction;
markTaskAsRunning: () => Promise;
run: () => Promise>;
@@ -129,11 +132,25 @@ export class TaskManagerRunner implements TaskRunner {
return this.definitions[this.taskType];
}
+ /**
+ * Gets the time at which this task will expire.
+ */
+ public get expiration() {
+ return intervalFromDate(this.instance.startedAt!, this.definition.timeout)!;
+ }
+
+ /**
+ * Gets the duration of the current task run
+ */
+ public get startedAt() {
+ return this.instance.startedAt;
+ }
+
/**
* Gets whether or not this task has run longer than its expiration setting allows.
*/
public get isExpired() {
- return intervalFromDate(this.instance.startedAt!, this.definition.timeout)! < new Date();
+ return this.expiration < new Date();
}
/**
@@ -261,12 +278,12 @@ export class TaskManagerRunner implements TaskRunner {
*/
public async cancel() {
const { task } = this;
- if (task && task.cancel) {
+ if (task?.cancel) {
this.task = undefined;
return task.cancel();
}
- this.logger.warn(`The task ${this} is not cancellable.`);
+ this.logger.debug(`The task ${this} is not cancellable.`);
}
private validateResult(result?: RunResult | void): Result {
diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts
index 4ecefcb7984eb4..8f122230d59653 100644
--- a/x-pack/plugins/task_manager/server/task_store.test.ts
+++ b/x-pack/plugins/task_manager/server/task_store.test.ts
@@ -17,9 +17,12 @@ import {
TaskLifecycleResult,
} from './task';
import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store';
-import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks';
-import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server';
-import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors';
+import { savedObjectsRepositoryMock } from 'src/core/server/mocks';
+import {
+ SavedObjectsSerializer,
+ SavedObjectTypeRegistry,
+ SavedObjectsErrorHelpers,
+} from 'src/core/server';
import { asTaskClaimEvent, TaskEvent } from './task_events';
import { asOk, asErr } from './lib/result_type';
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 1b77dfb168e886..3e681b8662e986 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8165,7 +8165,6 @@
"xpack.infra.viewSwitcher.mapViewLabel": "マップビュー",
"xpack.infra.viewSwitcher.tableViewLabel": "表ビュー",
"xpack.infra.waffle.accountAllTitle": "すべて",
- "xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}",
"xpack.infra.waffle.aggregationNames.avg": "{field} の平均",
"xpack.infra.waffle.aggregationNames.max": "{field} の最大値",
"xpack.infra.waffle.aggregationNames.min": "{field} の最小値",
@@ -8201,11 +8200,8 @@
"xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存",
"xpack.infra.waffle.customMetrics.submitLabel": "保存",
"xpack.infra.waffle.groupByAllTitle": "すべて",
- "xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ",
- "xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}",
"xpack.infra.waffle.loadingDataText": "データを読み込み中",
"xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです",
- "xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}",
"xpack.infra.waffle.metricOptions.countText": "カウント",
"xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況",
"xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り",
@@ -8232,7 +8228,6 @@
"xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。",
"xpack.infra.waffle.noDataTitle": "表示するデータがありません。",
"xpack.infra.waffle.region": "すべて",
- "xpack.infra.waffle.regionLabel": "地域: {selectedRegion}",
"xpack.infra.waffle.savedView.createHeader": "ビューを保存",
"xpack.infra.waffle.savedViews.cancel": "キャンセル",
"xpack.infra.waffle.savedViews.cancelButton": "キャンセル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ce2469f29b8833..8d50821bd8a588 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8168,7 +8168,6 @@
"xpack.infra.viewSwitcher.mapViewLabel": "地图视图",
"xpack.infra.viewSwitcher.tableViewLabel": "表视图",
"xpack.infra.waffle.accountAllTitle": "全部",
- "xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}",
"xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值",
"xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值",
"xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值",
@@ -8204,11 +8203,8 @@
"xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改",
"xpack.infra.waffle.customMetrics.submitLabel": "保存",
"xpack.infra.waffle.groupByAllTitle": "全部",
- "xpack.infra.waffle.groupByButtonLabel": "分组依据: ",
- "xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}",
"xpack.infra.waffle.loadingDataText": "正在加载数据",
"xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组",
- "xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}",
"xpack.infra.waffle.metricOptions.countText": "计数",
"xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用",
"xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取",
@@ -8235,7 +8231,6 @@
"xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。",
"xpack.infra.waffle.noDataTitle": "没有可显示的数据。",
"xpack.infra.waffle.region": "全部",
- "xpack.infra.waffle.regionLabel": "地区:{selectedRegion}",
"xpack.infra.waffle.savedView.createHeader": "保存视图",
"xpack.infra.waffle.savedViews.cancel": "取消",
"xpack.infra.waffle.savedViews.cancelButton": "取消",
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts
index 4228426d621597..5301f6364529d5 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts
@@ -6,7 +6,7 @@
import { ReindexStatus, ReindexStep } from '../../../../../../../common/types';
import { ReindexPollingService } from './polling_service';
-import { httpServiceMock } from 'src/core/public/http/http_service.mock';
+import { httpServiceMock } from 'src/core/public/mocks';
const mockClient = httpServiceMock.createSetupContract();
diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts
index b2b8ccf1ca57af..78b03275e0ef9f 100644
--- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts
@@ -5,7 +5,7 @@
*/
import { kibanaResponseFactory } from 'src/core/server';
-import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock';
+import { savedObjectsServiceMock } from 'src/core/server/mocks';
import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock';
import { createRequestMock } from './__mocks__/request.mock';
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
index e429de9ae0d68c..c94b5c96aa999f 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
@@ -6,7 +6,7 @@
import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
import { getMonitorStatus } from '../get_monitor_status';
-import { ScopedClusterClient } from 'src/core/server/elasticsearch';
+import { ScopedClusterClient } from 'src/core/server';
import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types';
interface BucketItemCriteria {
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
index c84b089d48c85e..e43f9dba4b2dc1 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
@@ -141,9 +141,6 @@ export default function getActionTests({ getService }: FtrProviderContext) {
actionTypeId: '.slack',
name: 'Slack#xyz',
isPreconfigured: true,
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
});
break;
default:
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
index 0b637326d46678..95b564e63d715b 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
@@ -73,11 +73,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
- config: {
- index: 'functional-test-actions-index-preconfigured',
- refresh: true,
- executionTimeField: 'timestamp',
- },
referencedByCount: 0,
},
{
@@ -85,9 +80,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
referencedByCount: 0,
},
{
@@ -95,11 +87,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
- config: {
- xyzConfig1: 'value1',
- xyzConfig2: 'value2',
- listOfThings: ['a', 'b', 'c', 'd'],
- },
referencedByCount: 0,
},
{
@@ -107,9 +94,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
- config: {
- unencrypted: 'ignored-but-required',
- },
referencedByCount: 0,
},
]);
@@ -194,11 +178,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
- config: {
- index: 'functional-test-actions-index-preconfigured',
- refresh: true,
- executionTimeField: 'timestamp',
- },
referencedByCount: 0,
},
{
@@ -206,9 +185,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
referencedByCount: 1,
},
{
@@ -216,11 +192,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
- config: {
- xyzConfig1: 'value1',
- xyzConfig2: 'value2',
- listOfThings: ['a', 'b', 'c', 'd'],
- },
referencedByCount: 0,
},
{
@@ -228,9 +199,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
- config: {
- unencrypted: 'ignored-but-required',
- },
referencedByCount: 0,
},
]);
@@ -281,11 +249,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
- config: {
- index: 'functional-test-actions-index-preconfigured',
- refresh: true,
- executionTimeField: 'timestamp',
- },
referencedByCount: 0,
},
{
@@ -293,9 +256,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
referencedByCount: 0,
},
{
@@ -303,11 +263,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
- config: {
- xyzConfig1: 'value1',
- xyzConfig2: 'value2',
- listOfThings: ['a', 'b', 'c', 'd'],
- },
referencedByCount: 0,
},
{
@@ -315,9 +270,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
- config: {
- unencrypted: 'ignored-but-required',
- },
referencedByCount: 0,
},
]);
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts
index a4a13441fb7668..4eb8c16f4fb3a8 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts
@@ -79,9 +79,6 @@ export default function getActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
});
});
});
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts
index ec59e56b08308b..62abdddc6a1bfb 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts
@@ -50,11 +50,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
- config: {
- index: 'functional-test-actions-index-preconfigured',
- refresh: true,
- executionTimeField: 'timestamp',
- },
referencedByCount: 0,
},
{
@@ -62,9 +57,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
referencedByCount: 0,
},
{
@@ -72,11 +64,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
- config: {
- xyzConfig1: 'value1',
- xyzConfig2: 'value2',
- listOfThings: ['a', 'b', 'c', 'd'],
- },
referencedByCount: 0,
},
{
@@ -84,9 +71,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
- config: {
- unencrypted: 'ignored-but-required',
- },
referencedByCount: 0,
},
]);
@@ -115,11 +99,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
- config: {
- index: 'functional-test-actions-index-preconfigured',
- refresh: true,
- executionTimeField: 'timestamp',
- },
referencedByCount: 0,
},
{
@@ -127,9 +106,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
- config: {
- webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
- },
referencedByCount: 0,
},
{
@@ -137,11 +113,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
- config: {
- xyzConfig1: 'value1',
- xyzConfig2: 'value2',
- listOfThings: ['a', 'b', 'c', 'd'],
- },
referencedByCount: 0,
},
{
@@ -149,9 +120,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
isPreconfigured: true,
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
- config: {
- unencrypted: 'ignored-but-required',
- },
referencedByCount: 0,
},
]);
diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts
index 41d78995711f2d..8af648e062cf44 100644
--- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts
+++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts
@@ -182,15 +182,21 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
service: { name: 'myservice', environment: 'development' },
settings: { transaction_sample_rate: '0.9' },
};
+ const configProduction = {
+ service: { name: 'myservice', environment: 'production' },
+ settings: { transaction_sample_rate: '0.9' },
+ };
let etag: string;
before(async () => {
log.debug('creating agent configuration');
await createConfiguration(config);
+ await createConfiguration(configProduction);
});
after(async () => {
await deleteConfiguration(config);
+ await deleteConfiguration(configProduction);
});
it(`should have 'applied_by_agent=false' before supplying etag`, async () => {
@@ -210,17 +216,45 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
});
it(`should have 'applied_by_agent=true' after supplying etag`, async () => {
- async function getAppliedByAgent() {
+ await searchConfigurations({
+ service: { name: 'myservice', environment: 'development' },
+ etag,
+ });
+
+ async function hasBeenAppliedByAgent() {
const { body } = await searchConfigurations({
service: { name: 'myservice', environment: 'development' },
- etag,
});
return body._source.applied_by_agent;
}
// wait until `applied_by_agent` has been updated in elasticsearch
- expect(await waitFor(getAppliedByAgent)).to.be(true);
+ expect(await waitFor(hasBeenAppliedByAgent)).to.be(true);
+ });
+ it(`should have 'applied_by_agent=false' before marking as applied`, async () => {
+ const res1 = await searchConfigurations({
+ service: { name: 'myservice', environment: 'production' },
+ });
+
+ expect(res1.body._source.applied_by_agent).to.be(false);
+ });
+ it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => {
+ await searchConfigurations({
+ service: { name: 'myservice', environment: 'production' },
+ mark_as_applied_by_agent: true,
+ });
+
+ async function hasBeenAppliedByAgent() {
+ const { body } = await searchConfigurations({
+ service: { name: 'myservice', environment: 'production' },
+ });
+
+ return body._source.applied_by_agent;
+ }
+
+ // wait until `applied_by_agent` has been updated in elasticsearch
+ expect(await waitFor(hasBeenAppliedByAgent)).to.be(true);
});
});
});
diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts
index 2acfca63995f1c..d33b92acf95a54 100644
--- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts
+++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts
@@ -44,6 +44,7 @@ export default function(providerContext: FtrProviderContext) {
});
// @ts-ignore
agentDoc.agents.access_api_key_id = accessAPIKeyId;
+ agentDoc.agents.default_api_key_id = outputAPIKeyBody.id;
agentDoc.agents.default_api_key = Buffer.from(
`${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}`
).toString('base64');
diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts
new file mode 100644
index 00000000000000..bbc766df34dcfe
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts
@@ -0,0 +1,145 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const jobId = `fq_single_${Date.now()}`;
+
+ const testDataList = [
+ {
+ testTitle: 'ML Poweruser creates a single metric job',
+ user: USER.ML_POWERUSER,
+ jobId: `${jobId}_1`,
+ requestBody: {
+ job_id: `${jobId}_1`,
+ description:
+ 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)',
+ groups: ['automated', 'farequote', 'single-metric'],
+ analysis_config: {
+ bucket_span: '30m',
+ detectors: [{ function: 'mean', field_name: 'responsetime' }],
+ influencers: [],
+ summary_count_field_name: 'doc_count',
+ },
+ data_description: { time_field: '@timestamp' },
+ analysis_limits: { model_memory_limit: '11MB' },
+ model_plot_config: { enabled: true },
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: {
+ job_id: `${jobId}_1`,
+ job_type: 'anomaly_detector',
+ groups: ['automated', 'farequote', 'single-metric'],
+ description:
+ 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)',
+ analysis_config: {
+ bucket_span: '30m',
+ summary_count_field_name: 'doc_count',
+ detectors: [
+ {
+ detector_description: 'mean(responsetime)',
+ function: 'mean',
+ field_name: 'responsetime',
+ detector_index: 0,
+ },
+ ],
+ influencers: [],
+ },
+ analysis_limits: { model_memory_limit: '11mb', categorization_examples_limit: 4 },
+ data_description: { time_field: '@timestamp', time_format: 'epoch_ms' },
+ model_plot_config: { enabled: true },
+ model_snapshot_retention_days: 1,
+ results_index_name: 'shared',
+ allow_lazy_open: false,
+ },
+ },
+ },
+ {
+ testTitle: 'ML viewer cannot create a job',
+ user: USER.ML_VIEWER,
+ jobId: `${jobId}_2`,
+ requestBody: {
+ job_id: `${jobId}_2`,
+ description:
+ 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)',
+ groups: ['automated', 'farequote', 'single-metric'],
+ analysis_config: {
+ bucket_span: '30m',
+ detectors: [{ function: 'mean', field_name: 'responsetime' }],
+ influencers: [],
+ summary_count_field_name: 'doc_count',
+ },
+ data_description: { time_field: '@timestamp' },
+ analysis_limits: { model_memory_limit: '11MB' },
+ model_plot_config: { enabled: true },
+ },
+ expected: {
+ responseCode: 403,
+ responseBody: {
+ statusCode: 403,
+ error: 'Forbidden',
+ message:
+ '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]',
+ },
+ },
+ },
+ ];
+
+ describe('create', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ for (const testData of testDataList) {
+ it(`${testData.testTitle}`, async () => {
+ const { body } = await supertest
+ .put(`/api/ml/anomaly_detectors/${testData.jobId}`)
+ .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user))
+ .set(COMMON_HEADERS)
+ .send(testData.requestBody)
+ .expect(testData.expected.responseCode);
+
+ if (body.error === undefined) {
+ // Validate the important parts of the response.
+ const expectedResponse = testData.expected.responseBody;
+ expect(body.job_id).to.eql(expectedResponse.job_id);
+ expect(body.groups).to.eql(expectedResponse.groups);
+ expect(body.analysis_config!.bucket_span).to.eql(
+ expectedResponse.analysis_config!.bucket_span
+ );
+ expect(body.analysis_config.detectors).to.have.length(
+ expectedResponse.analysis_config!.detectors.length
+ );
+ expect(body.analysis_config.detectors[0]).to.eql(
+ expectedResponse.analysis_config!.detectors[0]
+ );
+ } else {
+ expect(body.error).to.eql(testData.expected.responseBody.error);
+ expect(body.message).to.eql(testData.expected.responseBody.message);
+ }
+ });
+ }
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts
new file mode 100644
index 00000000000000..fb8acaf5c3ae98
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('anomaly detectors', function() {
+ loadTestFile(require.resolve('./create'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts
new file mode 100644
index 00000000000000..dfa81b5d78c653
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts
@@ -0,0 +1,248 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const metricFieldsTestData = {
+ testTitle: 'returns stats for metric fields over all time',
+ index: 'ft_farequote',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: {
+ bool: {
+ must: {
+ term: { airline: 'JZA' }, // Only use one airline to ensure no sampling.
+ },
+ },
+ },
+ fields: [
+ { type: 'number', cardinality: 0 },
+ { fieldName: 'responsetime', type: 'number', cardinality: 4249 },
+ ],
+ samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run.
+ timeFieldName: '@timestamp',
+ interval: '1d',
+ maxExamples: 10,
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: [
+ {
+ documentCounts: {
+ interval: '1d',
+ buckets: {
+ '1454803200000': 846,
+ '1454889600000': 846,
+ '1454976000000': 859,
+ '1455062400000': 851,
+ '1455148800000': 858,
+ },
+ },
+ },
+ {
+ // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic.
+ fieldName: 'responsetime',
+ count: 4260,
+ min: 963.4293212890625,
+ max: 1042.13525390625,
+ avg: 1000.0378077547315,
+ isTopValuesSampled: false,
+ topValues: [
+ { key: 980.0411987304688, doc_count: 2 },
+ { key: 989.278076171875, doc_count: 2 },
+ { key: 989.763916015625, doc_count: 2 },
+ { key: 991.290771484375, doc_count: 2 },
+ { key: 992.0765991210938, doc_count: 2 },
+ { key: 993.8115844726562, doc_count: 2 },
+ { key: 993.8973999023438, doc_count: 2 },
+ { key: 994.0230102539062, doc_count: 2 },
+ { key: 994.364990234375, doc_count: 2 },
+ { key: 994.916015625, doc_count: 2 },
+ ],
+ topValuesSampleSize: 4260,
+ topValuesSamplerShardSize: -1,
+ },
+ ],
+ },
+ };
+
+ const nonMetricFieldsTestData = {
+ testTitle: 'returns stats for non-metric fields specifying query and time range',
+ index: 'ft_farequote',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: {
+ bool: {
+ must: {
+ term: { airline: 'AAL' },
+ },
+ },
+ },
+ fields: [
+ { fieldName: '@timestamp', type: 'date', cardinality: 4751 },
+ { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 },
+ { fieldName: 'airline', type: 'keyword', cardinality: 19 },
+ { fieldName: 'type', type: 'text', cardinality: 0 },
+ { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 },
+ ],
+ samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run.
+ timeFieldName: '@timestamp',
+ earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT
+ latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT
+ maxExamples: 10,
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: [
+ { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 },
+ {
+ fieldName: '@version.keyword',
+ isTopValuesSampled: false,
+ topValues: [{ key: '1', doc_count: 1733 }],
+ topValuesSampleSize: 1733,
+ topValuesSamplerShardSize: -1,
+ },
+ {
+ fieldName: 'airline',
+ isTopValuesSampled: false,
+ topValues: [{ key: 'AAL', doc_count: 1733 }],
+ topValuesSampleSize: 1733,
+ topValuesSamplerShardSize: -1,
+ },
+ {
+ fieldName: 'type.keyword',
+ isTopValuesSampled: false,
+ topValues: [{ key: 'farequote', doc_count: 1733 }],
+ topValuesSampleSize: 1733,
+ topValuesSamplerShardSize: -1,
+ },
+ { fieldName: 'type', examples: ['farequote'] },
+ ],
+ },
+ };
+
+ const errorTestData = {
+ testTitle: 'returns error for index which does not exist',
+ index: 'ft_farequote_not_exists',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: { bool: { must: [{ match_all: {} }] } },
+ fields: [
+ { type: 'number', cardinality: 0 },
+ { fieldName: 'responsetime', type: 'number', cardinality: 4249 },
+ ],
+ samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run.
+ timeFieldName: '@timestamp',
+ maxExamples: 10,
+ },
+ expected: {
+ responseCode: 404,
+ responseBody: {
+ statusCode: 404,
+ error: 'Not Found',
+ message:
+ '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }',
+ },
+ },
+ };
+
+ async function runGetFieldStatsRequest(
+ index: string,
+ user: USER,
+ requestBody: object,
+ expectedResponsecode: number
+ ): Promise {
+ const { body } = await supertest
+ .post(`/api/ml/data_visualizer/get_field_stats/${index}`)
+ .auth(user, ml.securityCommon.getPasswordForUser(user))
+ .set(COMMON_HEADERS)
+ .send(requestBody)
+ .expect(expectedResponsecode);
+
+ return body;
+ }
+
+ function compareByFieldName(a: { fieldName: string }, b: { fieldName: string }) {
+ if (a.fieldName < b.fieldName) {
+ return -1;
+ }
+ if (a.fieldName > b.fieldName) {
+ return 1;
+ }
+ return 0;
+ }
+
+ describe('get_field_stats', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ it(`${metricFieldsTestData.testTitle}`, async () => {
+ const body = await runGetFieldStatsRequest(
+ metricFieldsTestData.index,
+ metricFieldsTestData.user,
+ metricFieldsTestData.requestBody,
+ metricFieldsTestData.expected.responseCode
+ );
+
+ // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic.
+ const expected = metricFieldsTestData.expected;
+ expect(body).to.have.length(expected.responseBody.length);
+
+ const actualDocCounts = body[0];
+ const expectedDocCounts = expected.responseBody[0];
+ expect(actualDocCounts).to.eql(expectedDocCounts);
+
+ const actualFieldData = { ...body[1] };
+ delete actualFieldData.median;
+ delete actualFieldData.distribution;
+
+ expect(actualFieldData).to.eql(expected.responseBody[1]);
+ });
+
+ it(`${nonMetricFieldsTestData.testTitle}`, async () => {
+ const body = await runGetFieldStatsRequest(
+ nonMetricFieldsTestData.index,
+ nonMetricFieldsTestData.user,
+ nonMetricFieldsTestData.requestBody,
+ nonMetricFieldsTestData.expected.responseCode
+ );
+
+ // Sort the fields in the response before validating.
+ const expectedRspFields = nonMetricFieldsTestData.expected.responseBody.sort(
+ compareByFieldName
+ );
+ const actualRspFields = body.sort(compareByFieldName);
+ expect(actualRspFields).to.eql(expectedRspFields);
+ });
+
+ it(`${errorTestData.testTitle}`, async () => {
+ const body = await runGetFieldStatsRequest(
+ errorTestData.index,
+ errorTestData.user,
+ errorTestData.requestBody,
+ errorTestData.expected.responseCode
+ );
+
+ expect(body.error).to.eql(errorTestData.expected.responseBody.error);
+ expect(body.message).to.eql(errorTestData.expected.responseBody.message);
+ });
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts
new file mode 100644
index 00000000000000..6490c19c644834
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts
@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const testDataList = [
+ {
+ testTitle: 'returns stats over all time',
+ index: 'ft_farequote',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: { bool: { must: [{ match_all: {} }] } },
+ aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'],
+ nonAggregatableFields: ['type'],
+ samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run.
+ timeFieldName: '@timestamp',
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: {
+ totalCount: 86274,
+ aggregatableExistsFields: [
+ {
+ fieldName: '@timestamp',
+ existsInDocs: true,
+ stats: { sampleCount: 86274, count: 86274, cardinality: 78580 },
+ },
+ {
+ fieldName: 'airline',
+ existsInDocs: true,
+ stats: { sampleCount: 86274, count: 86274, cardinality: 19 },
+ },
+ {
+ fieldName: 'responsetime',
+ existsInDocs: true,
+ stats: { sampleCount: 86274, count: 86274, cardinality: 83346 },
+ },
+ ],
+ aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }],
+ nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }],
+ nonAggregatableNotExistsFields: [],
+ },
+ },
+ },
+ {
+ testTitle: 'returns stats when specifying query and time range',
+ index: 'ft_farequote',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: {
+ bool: {
+ must: {
+ term: { airline: 'AAL' },
+ },
+ },
+ },
+ aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'],
+ nonAggregatableFields: ['type'],
+ samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run.
+ timeFieldName: '@timestamp',
+ earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT
+ latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: {
+ totalCount: 1733,
+ aggregatableExistsFields: [
+ {
+ fieldName: '@timestamp',
+ existsInDocs: true,
+ stats: { sampleCount: 1733, count: 1733, cardinality: 1713 },
+ },
+ {
+ fieldName: 'airline',
+ existsInDocs: true,
+ stats: { sampleCount: 1733, count: 1733, cardinality: 1 },
+ },
+ {
+ fieldName: 'responsetime',
+ existsInDocs: true,
+ stats: { sampleCount: 1733, count: 1733, cardinality: 1730 },
+ },
+ ],
+ aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }],
+ nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }],
+ nonAggregatableNotExistsFields: [],
+ },
+ },
+ },
+ {
+ testTitle: 'returns error for index which does not exist',
+ index: 'ft_farequote_not_exist',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ query: { bool: { must: [{ match_all: {} }] } },
+ aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'],
+ nonAggregatableFields: ['@version', 'type'],
+ samplerShardSize: 1000,
+ timeFieldName: '@timestamp',
+ },
+ expected: {
+ responseCode: 404,
+ responseBody: {
+ statusCode: 404,
+ error: 'Not Found',
+ message:
+ '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }',
+ },
+ },
+ },
+ ];
+
+ describe('get_overall_stats', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ for (const testData of testDataList) {
+ it(`${testData.testTitle}`, async () => {
+ const { body } = await supertest
+ .post(`/api/ml/data_visualizer/get_overall_stats/${testData.index}`)
+ .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user))
+ .set(COMMON_HEADERS)
+ .send(testData.requestBody)
+ .expect(testData.expected.responseCode);
+
+ if (body.error === undefined) {
+ expect(body).to.eql(testData.expected.responseBody);
+ } else {
+ expect(body.error).to.eql(testData.expected.responseBody.error);
+ expect(body.message).to.eql(testData.expected.responseBody.message);
+ }
+ });
+ }
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts
new file mode 100644
index 00000000000000..ce9e44618f1afc
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('data visualizer', function() {
+ loadTestFile(require.resolve('./get_field_stats'));
+ loadTestFile(require.resolve('./get_overall_stats'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts
new file mode 100644
index 00000000000000..245375562b5c1f
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const testDataList = [
+ {
+ testTitle: 'returns cardinality of customer name fields over full time range',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce',
+ fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'],
+ query: { bool: { must: [{ match_all: {} }] } },
+ timeFieldName: 'order_date',
+ },
+ expected: {
+ responseBody: {
+ 'customer_first_name.keyword': 46,
+ 'customer_last_name.keyword': 183,
+ },
+ },
+ },
+ {
+ testTitle: 'returns cardinality of geoip fields over specified range',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce',
+ fieldNames: ['geoip.city_name', 'geoip.continent_name', 'geoip.country_iso_code'],
+ query: { bool: { must: [{ match_all: {} }] } },
+ timeFieldName: 'order_date',
+ earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT
+ latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT
+ },
+ expected: {
+ responseBody: {
+ 'geoip.city_name': 10,
+ 'geoip.continent_name': 5,
+ 'geoip.country_iso_code': 9,
+ },
+ },
+ },
+ {
+ testTitle: 'returns empty response for non aggregatable field',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce',
+ fieldNames: ['manufacturer'],
+ query: { bool: { must: [{ match_all: {} }] } },
+ timeFieldName: 'order_date',
+ earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT
+ latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT
+ },
+ expected: {
+ responseBody: {},
+ },
+ },
+ {
+ testTitle: 'returns error for index which does not exist',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce_not_exist',
+ fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'],
+ timeFieldName: 'order_date',
+ },
+ expected: {
+ responseBody: {
+ statusCode: 404,
+ error: 'Not Found',
+ message:
+ '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }',
+ },
+ },
+ },
+ ];
+
+ describe('field_cardinality', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ for (const testData of testDataList) {
+ it(`${testData.testTitle}`, async () => {
+ const { body } = await supertest
+ .post('/api/ml/fields_service/field_cardinality')
+ .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user))
+ .set(COMMON_HEADERS)
+ .send(testData.requestBody);
+
+ if (body.error === undefined) {
+ expect(body).to.eql(testData.expected.responseBody);
+ } else {
+ expect(body.error).to.eql(testData.expected.responseBody.error);
+ expect(body.message).to.eql(testData.expected.responseBody.message);
+ }
+ });
+ }
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/fields_service/index.ts b/x-pack/test/api_integration/apis/ml/fields_service/index.ts
new file mode 100644
index 00000000000000..312602e5891196
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/fields_service/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('fields service', function() {
+ loadTestFile(require.resolve('./field_cardinality'));
+ loadTestFile(require.resolve('./time_field_range'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts
new file mode 100644
index 00000000000000..2f0fd4fc6c5e37
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const testDataList = [
+ {
+ testTitle: 'returns expected time range with index and match_all query',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce',
+ query: { bool: { must: [{ match_all: {} }] } },
+ timeFieldName: 'order_date',
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: {
+ start: {
+ epoch: 1560297859000,
+ string: '2019-06-12T00:04:19.000Z',
+ },
+ end: {
+ epoch: 1562975136000,
+ string: '2019-07-12T23:45:36.000Z',
+ },
+ success: true,
+ },
+ },
+ },
+ {
+ testTitle: 'returns expected time range with index and query',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce',
+ query: {
+ term: {
+ 'customer_first_name.keyword': {
+ value: 'Brigitte',
+ },
+ },
+ },
+ timeFieldName: 'order_date',
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: {
+ start: {
+ epoch: 1560298982000,
+ string: '2019-06-12T00:23:02.000Z',
+ },
+ end: {
+ epoch: 1562973754000,
+ string: '2019-07-12T23:22:34.000Z',
+ },
+ success: true,
+ },
+ },
+ },
+ {
+ testTitle: 'returns error for index which does not exist',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ index: 'ft_ecommerce_not_exist',
+ query: { bool: { must: [{ match_all: {} }] } },
+ timeFieldName: 'order_date',
+ },
+ expected: {
+ responseCode: 404,
+ responseBody: {
+ statusCode: 404,
+ error: 'Not Found',
+ message:
+ '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }',
+ },
+ },
+ },
+ ];
+
+ describe('time_field_range', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ for (const testData of testDataList) {
+ it(`${testData.testTitle}`, async () => {
+ const { body } = await supertest
+ .post('/api/ml/fields_service/time_field_range')
+ .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user))
+ .set(COMMON_HEADERS)
+ .send(testData.requestBody)
+ .expect(testData.expected.responseCode);
+
+ if (body.error === undefined) {
+ expect(body).to.eql(testData.expected.responseBody);
+ } else {
+ expect(body.error).to.eql(testData.expected.responseBody.error);
+ expect(body.message).to.eql(testData.expected.responseBody.message);
+ }
+ });
+ }
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts
index f012883c46ca3a..58356637c63ac5 100644
--- a/x-pack/test/api_integration/apis/ml/index.ts
+++ b/x-pack/test/api_integration/apis/ml/index.ts
@@ -31,11 +31,11 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
await ml.testResources.resetKibanaTimeZone();
});
- loadTestFile(require.resolve('./bucket_span_estimator'));
- loadTestFile(require.resolve('./calculate_model_memory_limit'));
- loadTestFile(require.resolve('./categorization_field_examples'));
- loadTestFile(require.resolve('./get_module'));
- loadTestFile(require.resolve('./recognize_module'));
- loadTestFile(require.resolve('./setup_module'));
+ loadTestFile(require.resolve('./modules'));
+ loadTestFile(require.resolve('./anomaly_detectors'));
+ loadTestFile(require.resolve('./data_visualizer'));
+ loadTestFile(require.resolve('./fields_service'));
+ loadTestFile(require.resolve('./job_validation'));
+ loadTestFile(require.resolve('./jobs'));
});
}
diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts
similarity index 97%
rename from x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts
rename to x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts
index bc0dc3019d7c99..0b4aca9660be44 100644
--- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts
+++ b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts
@@ -6,8 +6,8 @@
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts
similarity index 97%
rename from x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts
rename to x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts
index 59e3dfcca00f9c..f17814633ce8fe 100644
--- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts
+++ b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { FtrProviderContext } from '../../ftr_provider_context';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts
new file mode 100644
index 00000000000000..6ca9dcbbe9e5b8
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('job validation', function() {
+ loadTestFile(require.resolve('./bucket_span_estimator'));
+ loadTestFile(require.resolve('./calculate_model_memory_limit'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts
similarity index 98%
rename from x-pack/test/api_integration/apis/ml/categorization_field_examples.ts
rename to x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts
index df0153f9659421..bcc6c4907100c4 100644
--- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts
+++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts
@@ -6,8 +6,8 @@
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts
new file mode 100644
index 00000000000000..70a64f198d6f4b
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('jobs', function() {
+ loadTestFile(require.resolve('./categorization_field_examples'));
+ loadTestFile(require.resolve('./jobs_summary'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts
new file mode 100644
index 00000000000000..6a57db1687868f
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts
@@ -0,0 +1,374 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
+import { Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';
+
+const COMMON_HEADERS = {
+ 'kbn-xsrf': 'some-xsrf-token',
+};
+
+const SINGLE_METRIC_JOB_CONFIG: Job = {
+ job_id: `jobs_summary_fq_single_${Date.now()}`,
+ description: 'mean(responsetime) on farequote dataset with 15m bucket span',
+ groups: ['farequote', 'automated', 'single-metric'],
+ analysis_config: {
+ bucket_span: '15m',
+ influencers: [],
+ detectors: [
+ {
+ function: 'mean',
+ field_name: 'responsetime',
+ },
+ ],
+ },
+ data_description: { time_field: '@timestamp' },
+ analysis_limits: { model_memory_limit: '10mb' },
+ model_plot_config: { enabled: true },
+};
+
+const MULTI_METRIC_JOB_CONFIG: Job = {
+ job_id: `jobs_summary_fq_multi_${Date.now()}`,
+ description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span',
+ groups: ['farequote', 'automated', 'multi-metric'],
+ analysis_config: {
+ bucket_span: '1h',
+ influencers: ['airline'],
+ detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }],
+ },
+ data_description: { time_field: '@timestamp' },
+ analysis_limits: { model_memory_limit: '20mb' },
+ model_plot_config: { enabled: true },
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG];
+
+ const testDataListNoJobId = [
+ {
+ testTitle: 'as ML Poweruser',
+ user: USER.ML_POWERUSER,
+ requestBody: {},
+ expected: {
+ responseCode: 200,
+ responseBody: [
+ {
+ id: SINGLE_METRIC_JOB_CONFIG.job_id,
+ description: SINGLE_METRIC_JOB_CONFIG.description,
+ groups: SINGLE_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ },
+ {
+ id: MULTI_METRIC_JOB_CONFIG.job_id,
+ description: MULTI_METRIC_JOB_CONFIG.description,
+ groups: MULTI_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ },
+ ],
+ },
+ },
+ {
+ testTitle: 'as ML Viewer',
+ user: USER.ML_VIEWER,
+ requestBody: {},
+ expected: {
+ responseCode: 200,
+ responseBody: [
+ {
+ id: SINGLE_METRIC_JOB_CONFIG.job_id,
+ description: SINGLE_METRIC_JOB_CONFIG.description,
+ groups: SINGLE_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ },
+ {
+ id: MULTI_METRIC_JOB_CONFIG.job_id,
+ description: MULTI_METRIC_JOB_CONFIG.description,
+ groups: MULTI_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ },
+ ],
+ },
+ },
+ ];
+
+ const testDataListWithJobId = [
+ {
+ testTitle: 'as ML Poweruser',
+ user: USER.ML_POWERUSER,
+ requestBody: {
+ jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id],
+ },
+ expected: {
+ responseCode: 200,
+ responseBody: [
+ {
+ id: SINGLE_METRIC_JOB_CONFIG.job_id,
+ description: SINGLE_METRIC_JOB_CONFIG.description,
+ groups: SINGLE_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ fullJob: {
+ // Only tests against some of the fields in the fullJob property.
+ job_id: SINGLE_METRIC_JOB_CONFIG.job_id,
+ job_type: 'anomaly_detector',
+ description: SINGLE_METRIC_JOB_CONFIG.description,
+ groups: SINGLE_METRIC_JOB_CONFIG.groups,
+ analysis_config: {
+ bucket_span: '15m',
+ detectors: [
+ {
+ detector_description: 'mean(responsetime)',
+ function: 'mean',
+ field_name: 'responsetime',
+ detector_index: 0,
+ },
+ ],
+ influencers: [],
+ },
+ },
+ },
+ {
+ id: MULTI_METRIC_JOB_CONFIG.job_id,
+ description: MULTI_METRIC_JOB_CONFIG.description,
+ groups: MULTI_METRIC_JOB_CONFIG.groups,
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: false,
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ isSingleMetricViewerJob: true,
+ },
+ ],
+ },
+ },
+ ];
+
+ const testDataListNegative = [
+ {
+ testTitle: 'as ML Unauthorized user',
+ user: USER.ML_UNAUTHORIZED,
+ requestBody: {},
+ // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic.
+ expected: {
+ responseCode: 403,
+ error: 'Forbidden',
+ },
+ },
+ ];
+
+ async function runJobsSummaryRequest(
+ user: USER,
+ requestBody: object,
+ expectedResponsecode: number
+ ): Promise {
+ const { body } = await supertest
+ .post('/api/ml/jobs/jobs_summary')
+ .auth(user, ml.securityCommon.getPasswordForUser(user))
+ .set(COMMON_HEADERS)
+ .send(requestBody)
+ .expect(expectedResponsecode);
+
+ return body;
+ }
+
+ function compareById(a: { id: string }, b: { id: string }) {
+ if (a.id < b.id) {
+ return -1;
+ }
+ if (a.id > b.id) {
+ return 1;
+ }
+ return 0;
+ }
+
+ function getGroups(jobs: Array<{ groups: string[] }>) {
+ const groupIds: string[] = [];
+ jobs.forEach(job => {
+ const groups = job.groups;
+ groups.forEach(group => {
+ if (groupIds.indexOf(group) === -1) {
+ groupIds.push(group);
+ }
+ });
+ });
+ return groupIds.sort();
+ }
+
+ describe('jobs_summary', function() {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ it('sets up jobs', async () => {
+ for (const job of testSetupJobConfigs) {
+ await ml.api.createAnomalyDetectionJob(job);
+ }
+ });
+
+ for (const testData of testDataListNoJobId) {
+ describe('gets job summary with no job IDs supplied', function() {
+ it(`${testData.testTitle}`, async () => {
+ const body = await runJobsSummaryRequest(
+ testData.user,
+ testData.requestBody,
+ testData.expected.responseCode
+ );
+
+ // Validate the important parts of the response.
+ const expectedResponse = testData.expected.responseBody;
+
+ // Validate job count.
+ expect(body).to.have.length(expectedResponse.length);
+
+ // Validate job IDs.
+ const expectedRspJobIds = expectedResponse
+ .map((job: { id: string }) => {
+ return { id: job.id };
+ })
+ .sort(compareById);
+ const actualRspJobIds = body
+ .map((job: { id: string }) => {
+ return { id: job.id };
+ })
+ .sort(compareById);
+
+ expect(actualRspJobIds).to.eql(expectedRspJobIds);
+
+ // Validate created group IDs.
+ const expectedRspGroupIds = getGroups(expectedResponse);
+ const actualRspGroupsIds = getGroups(body);
+ expect(actualRspGroupsIds).to.eql(expectedRspGroupIds);
+ });
+ });
+ }
+
+ for (const testData of testDataListWithJobId) {
+ describe('gets job summary with job ID supplied', function() {
+ it(`${testData.testTitle}`, async () => {
+ const body = await runJobsSummaryRequest(
+ testData.user,
+ testData.requestBody,
+ testData.expected.responseCode
+ );
+
+ // Validate the important parts of the response.
+ const expectedResponse = testData.expected.responseBody;
+
+ // Validate job count.
+ expect(body).to.have.length(expectedResponse.length);
+
+ // Validate job IDs.
+ const expectedRspJobIds = expectedResponse
+ .map((job: { id: string }) => {
+ return { id: job.id };
+ })
+ .sort(compareById);
+ const actualRspJobIds = body
+ .map((job: { id: string }) => {
+ return { id: job.id };
+ })
+ .sort(compareById);
+
+ expect(actualRspJobIds).to.eql(expectedRspJobIds);
+
+ // Validate created group IDs.
+ const expectedRspGroupIds = getGroups(expectedResponse);
+ const actualRspGroupsIds = getGroups(body);
+ expect(actualRspGroupsIds).to.eql(expectedRspGroupIds);
+
+ // Validate the response for the specified job IDs contains a fullJob property.
+ const requestedJobIds = testData.requestBody.jobIds;
+ for (const job of body) {
+ if (requestedJobIds.includes(job.id)) {
+ expect(job).to.have.property('fullJob');
+ } else {
+ expect(job).not.to.have.property('fullJob');
+ }
+ }
+
+ for (const expectedJob of expectedResponse) {
+ const expectedJobId = expectedJob.id;
+ const actualJob = body.find((job: { id: string }) => job.id === expectedJobId);
+ if (expectedJob.fullJob) {
+ expect(actualJob).to.have.property('fullJob');
+ expect(actualJob.fullJob).to.have.property('analysis_config');
+ expect(actualJob.fullJob.analysis_config).to.eql(expectedJob.fullJob.analysis_config);
+ } else {
+ expect(actualJob).not.to.have.property('fullJob');
+ }
+ }
+ });
+ });
+ }
+
+ for (const testData of testDataListNegative) {
+ describe('rejects request', function() {
+ it(testData.testTitle, async () => {
+ const body = await runJobsSummaryRequest(
+ testData.user,
+ testData.requestBody,
+ testData.expected.responseCode
+ );
+
+ expect(body)
+ .to.have.property('error')
+ .eql(testData.expected.error);
+
+ expect(body).to.have.property('message');
+ });
+ });
+ }
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
similarity index 92%
rename from x-pack/test/api_integration/apis/ml/get_module.ts
rename to x-pack/test/api_integration/apis/ml/modules/get_module.ts
index a50d3c0abe4300..e19d45999c88e5 100644
--- a/x-pack/test/api_integration/apis/ml/get_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
@@ -6,8 +6,8 @@
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts
new file mode 100644
index 00000000000000..4fdc404c607aa9
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/modules/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('modules', function() {
+ loadTestFile(require.resolve('./get_module'));
+ loadTestFile(require.resolve('./recognize_module'));
+ loadTestFile(require.resolve('./setup_module'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
similarity index 93%
rename from x-pack/test/api_integration/apis/ml/recognize_module.ts
rename to x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
index 8e360579c1459f..948728189b8bd1 100644
--- a/x-pack/test/api_integration/apis/ml/recognize_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
@@ -6,8 +6,8 @@
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
similarity index 96%
rename from x-pack/test/api_integration/apis/ml/setup_module.ts
rename to x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index e603782b25717a..23ddd3b63a2eff 100644
--- a/x-pack/test/api_integration/apis/ml/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -6,10 +6,10 @@
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
+import { FtrProviderContext } from '../../../ftr_provider_context';
-import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states';
-import { USER } from '../../../functional/services/machine_learning/security_common';
+import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states';
+import { USER } from '../../../../functional/services/machine_learning/security_common';
const COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts
new file mode 100644
index 00000000000000..276a5367a419ee
--- /dev/null
+++ b/x-pack/test/api_integration/apis/security/api_keys.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 expect from '@kbn/expect/expect.js';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+
+ describe('API Keys', () => {
+ describe('GET /internal/security/api_key/_enabled', () => {
+ it('should indicate that API Keys are enabled', async () => {
+ await supertest
+ .get('/internal/security/api_key/_enabled')
+ .set('kbn-xsrf', 'xxx')
+ .send()
+ .expect(200)
+ .then((response: Record) => {
+ const payload = response.body;
+ expect(payload).to.eql({ apiKeysEnabled: true });
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js
index ad1876cb717f12..7bb79a589d522f 100644
--- a/x-pack/test/api_integration/apis/security/index.js
+++ b/x-pack/test/api_integration/apis/security/index.js
@@ -11,6 +11,7 @@ export default function({ loadTestFile }) {
// Updates here should be mirrored in `./security_basic.ts` if tests
// should also run under a basic license.
+ loadTestFile(require.resolve('./api_keys'));
loadTestFile(require.resolve('./basic_login'));
loadTestFile(require.resolve('./builtin_es_privileges'));
loadTestFile(require.resolve('./change_password'));
diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts
index dcbdb17724249b..3e426f210afa87 100644
--- a/x-pack/test/api_integration/apis/security/security_basic.ts
+++ b/x-pack/test/api_integration/apis/security/security_basic.ts
@@ -13,6 +13,7 @@ export default function({ loadTestFile }: FtrProviderContext) {
// Updates here should be mirrored in `./index.js` if tests
// should also run under a trial/platinum license.
+ loadTestFile(require.resolve('./api_keys'));
loadTestFile(require.resolve('./basic_login'));
loadTestFile(require.resolve('./builtin_es_privileges'));
loadTestFile(require.resolve('./change_password'));
diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.js
index d21bfa4d7031a7..4c4b77ee5b080f 100644
--- a/x-pack/test/api_integration/config_security_basic.js
+++ b/x-pack/test/api_integration/config_security_basic.js
@@ -13,6 +13,7 @@ export default async function({ readConfigFile }) {
config.esTestCluster.serverArgs = [
'xpack.license.self_generated.type=basic',
'xpack.security.enabled=true',
+ 'xpack.security.authc.api_key.enabled=true',
];
config.testFiles = [require.resolve('./apis/security/security_basic')];
return config;
diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts
new file mode 100644
index 00000000000000..f9c248ec3d56f5
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/config.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createTestConfig } from '../common/config';
+
+// eslint-disable-next-line import/no-default-export
+export default createTestConfig('basic', {
+ disabledPlugins: [],
+ license: 'basic',
+ ssl: true,
+});
diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts
new file mode 100644
index 00000000000000..a9fc2706a6ba20
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+
+import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants';
+import {
+ getConfiguration,
+ removeServerGeneratedPropertiesFromConfigure,
+ getConfigurationOutput,
+ deleteConfiguration,
+} from '../../../common/lib/utils';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const es = getService('legacyEs');
+
+ describe('get_configure', () => {
+ afterEach(async () => {
+ await deleteConfiguration(es);
+ });
+
+ it('should return an empty find body correctly if no configuration is loaded', async () => {
+ const { body } = await supertest
+ .get(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ expect(body).to.eql({});
+ });
+
+ it('should return a configuration', async () => {
+ await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration())
+ .expect(200);
+
+ const { body } = await supertest
+ .get(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ const data = removeServerGeneratedPropertiesFromConfigure(body);
+ expect(data).to.eql(getConfigurationOutput());
+ });
+ });
+};
diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts
new file mode 100644
index 00000000000000..836c76d500034b
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+
+import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/case/common/constants';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+
+ describe('get_connectors', () => {
+ it('should return an empty find body correctly if no connectors are loaded', async () => {
+ const { body } = await supertest
+ .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ expect(body).to.eql([]);
+ });
+ });
+};
diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts
new file mode 100644
index 00000000000000..d66baa2a2eee2b
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+
+import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants';
+import {
+ getConfiguration,
+ removeServerGeneratedPropertiesFromConfigure,
+ getConfigurationOutput,
+ deleteConfiguration,
+} from '../../../common/lib/utils';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const es = getService('legacyEs');
+
+ describe('post_configure', () => {
+ afterEach(async () => {
+ await deleteConfiguration(es);
+ });
+
+ it('should patch a configuration', async () => {
+ const res = await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration())
+ .expect(200);
+
+ const { body } = await supertest
+ .patch(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send({ closure_type: 'close-by-pushing', version: res.body.version })
+ .expect(200);
+
+ const data = removeServerGeneratedPropertiesFromConfigure(body);
+ expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' });
+ });
+
+ it('should handle patch request when there is no configuration', async () => {
+ const { body } = await supertest
+ .patch(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send({ closure_type: 'close-by-pushing', version: 'no-version' })
+ .expect(409);
+
+ expect(body).to.eql({
+ error: 'Conflict',
+ message:
+ 'You can not patch this configuration since you did not created first with a post.',
+ statusCode: 409,
+ });
+ });
+
+ it('should handle patch request when versions are different', async () => {
+ await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration())
+ .expect(200);
+
+ const { body } = await supertest
+ .patch(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send({ closure_type: 'close-by-pushing', version: 'no-version' })
+ .expect(409);
+
+ expect(body).to.eql({
+ error: 'Conflict',
+ message:
+ 'This configuration has been updated. Please refresh before saving additional updates.',
+ statusCode: 409,
+ });
+ });
+ });
+};
diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts
new file mode 100644
index 00000000000000..c2284492e5b771
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+
+import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants';
+import {
+ getConfiguration,
+ removeServerGeneratedPropertiesFromConfigure,
+ getConfigurationOutput,
+ deleteConfiguration,
+} from '../../../common/lib/utils';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const es = getService('legacyEs');
+
+ describe('post_configure', () => {
+ afterEach(async () => {
+ await deleteConfiguration(es);
+ });
+
+ it('should create a configuration', async () => {
+ const { body } = await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration())
+ .expect(200);
+
+ const data = removeServerGeneratedPropertiesFromConfigure(body);
+ expect(data).to.eql(getConfigurationOutput());
+ });
+
+ it('should keep only the latest configuration', async () => {
+ await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration('connector-2'))
+ .expect(200);
+
+ await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send(getConfiguration())
+ .expect(200);
+
+ const { body } = await supertest
+ .get(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ const data = removeServerGeneratedPropertiesFromConfigure(body);
+ expect(data).to.eql(getConfigurationOutput());
+ });
+ });
+};
diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts
new file mode 100644
index 00000000000000..efd5369c019d82
--- /dev/null
+++ b/x-pack/test/case_api_integration/basic/tests/index.ts
@@ -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 { FtrProviderContext } from '../../common/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ loadTestFile }: FtrProviderContext): void => {
+ describe('case api basic', function() {
+ // Fastest ciGroup for the moment.
+ this.tags('ciGroup2');
+
+ loadTestFile(require.resolve('./configure/get_configure'));
+ loadTestFile(require.resolve('./configure/post_configure'));
+ loadTestFile(require.resolve('./configure/patch_configure'));
+ loadTestFile(require.resolve('./configure/get_connectors'));
+ });
+};
diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts
new file mode 100644
index 00000000000000..862705ab9610b6
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/config.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import path from 'path';
+
+import { CA_CERT_PATH } from '@kbn/dev-utils';
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+
+import { services } from './services';
+
+interface CreateTestConfigOptions {
+ license: string;
+ disabledPlugins?: string[];
+ ssl?: boolean;
+}
+
+// test.not-enabled is specifically not enabled
+const enabledActionTypes = [
+ '.email',
+ '.index',
+ '.pagerduty',
+ '.server-log',
+ '.servicenow',
+ '.slack',
+ '.webhook',
+ 'test.authorization',
+ 'test.failing',
+ 'test.index-record',
+ 'test.noop',
+ 'test.rate-limit',
+];
+
+export function createTestConfig(name: string, options: CreateTestConfigOptions) {
+ const { license = 'trial', disabledPlugins = [], ssl = false } = options;
+
+ return async ({ readConfigFile }: FtrConfigProviderContext) => {
+ const xPackApiIntegrationTestsConfig = await readConfigFile(
+ require.resolve('../../api_integration/config.js')
+ );
+
+ const servers = {
+ ...xPackApiIntegrationTestsConfig.get('servers'),
+ elasticsearch: {
+ ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'),
+ protocol: ssl ? 'https' : 'http',
+ },
+ };
+
+ return {
+ testFiles: [require.resolve(`../${name}/tests/`)],
+ servers,
+ services,
+ junit: {
+ reportName: 'X-Pack Case API Integration Tests',
+ },
+ esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'),
+ esTestCluster: {
+ ...xPackApiIntegrationTestsConfig.get('esTestCluster'),
+ license,
+ ssl,
+ serverArgs: [
+ `xpack.license.self_generated.type=${license}`,
+ `xpack.security.enabled=${!disabledPlugins.includes('security') &&
+ ['trial', 'basic'].includes(license)}`,
+ ],
+ },
+ kbnTestServer: {
+ ...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
+ `--xpack.actions.whitelistedHosts=${JSON.stringify([
+ 'localhost',
+ 'some.non.existent.com',
+ ])}`,
+ `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
+ '--xpack.alerting.enabled=true',
+ '--xpack.eventLog.logEntries=true',
+ ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
+ `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
+ `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
+ ...(ssl
+ ? [
+ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
+ `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
+ ]
+ : []),
+ ],
+ },
+ };
+ };
+}
diff --git a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts
new file mode 100644
index 00000000000000..e3add3748f56d7
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
+
+import { services } from './services';
+
+export type FtrProviderContext = GenericFtrProviderContext;
diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts
new file mode 100644
index 00000000000000..6d0db69309b908
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/lib/utils.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api';
+
+export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => {
+ return {
+ connector_id,
+ connector_name: 'Connector 1',
+ closure_type: 'close-by-user',
+ };
+};
+
+export const getConfigurationOutput = (update = false): Partial => {
+ return {
+ ...getConfiguration(),
+ created_by: { email: null, full_name: null, username: 'elastic' },
+ updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null,
+ };
+};
+
+export const removeServerGeneratedPropertiesFromConfigure = (
+ config: Partial
+): Partial => {
+ const { created_at, updated_at, version, ...rest } = config;
+ return rest;
+};
+
+export const deleteConfiguration = async (es: any): Promise => {
+ await es.deleteByQuery({
+ index: '.kibana',
+ q: 'type:cases-configure',
+ waitForCompletion: true,
+ refresh: 'wait_for',
+ body: {},
+ });
+};
+
+export const getConnector = () => ({
+ name: 'ServiceNow Connector',
+ actionTypeId: '.servicenow',
+ secrets: {
+ username: 'admin',
+ password: 'admin',
+ },
+ config: {
+ apiUrl: 'localhost',
+ casesConfiguration: {
+ mapping: [
+ {
+ source: 'title',
+ target: 'short_description',
+ actionType: 'overwrite',
+ },
+ {
+ source: 'description',
+ target: 'description',
+ actionType: 'overwrite',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ actionType: 'append',
+ },
+ ],
+ },
+ },
+});
diff --git a/x-pack/test/case_api_integration/common/services.ts b/x-pack/test/case_api_integration/common/services.ts
new file mode 100644
index 00000000000000..a927a31469bab1
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/services.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 { services } from '../../api_integration/services';
diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js
index 8ab84126b2b30a..3bcf504c17a6fd 100644
--- a/x-pack/test/functional/apps/security/management.js
+++ b/x-pack/test/functional/apps/security/management.js
@@ -19,7 +19,8 @@ export default function({ getService, getPageObjects }) {
const browser = getService('browser');
const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']);
- describe('Management', function() {
+ // FLAKY: https://github.com/elastic/kibana/issues/61173
+ describe.skip('Management', function() {
this.tags(['skipFirefox']);
before(async () => {
diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts
index 3b11ef61a1ab21..0b127288e79583 100644
--- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts
+++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts
@@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) {
});
}
- async function checkSessionCookie(sessionCookie: Cookie) {
+ async function checkSessionCookie(sessionCookie: Cookie, username = 'a@b.c') {
expect(sessionCookie.key).to.be('sid');
expect(sessionCookie.value).to.not.be.empty();
expect(sessionCookie.path).to.be('/');
@@ -59,7 +59,7 @@ export default function({ getService }: FtrProviderContext) {
'authentication_provider',
]);
- expect(apiResponse.body.username).to.be('a@b.c');
+ expect(apiResponse.body.username).to.be(username);
}
describe('SAML authentication', () => {
@@ -668,6 +668,29 @@ export default function({ getService }: FtrProviderContext) {
const existingUsername = 'a@b.c';
let existingSessionCookie: Cookie;
+ const testScenarios: Array<[string, () => Promise]> = [
+ // Default scenario when active cookie has an active access token.
+ ['when access token is valid', async () => {}],
+ // Scenario when active cookie has an expired access token. Access token expiration is set
+ // to 15s for API integration tests so we need to wait for 20s to make sure token expires.
+ ['when access token is expired', async () => await delay(20000)],
+ // Scenario when active cookie references to access/refresh token pair that were already
+ // removed from Elasticsearch (to simulate 24h when expired tokens are removed).
+ [
+ 'when access token document is missing',
+ async () => {
+ const esResponse = await getService('legacyEs').deleteByQuery({
+ index: '.security-tokens',
+ q: 'doc_type:token',
+ refresh: true,
+ });
+ expect(esResponse)
+ .to.have.property('deleted')
+ .greaterThan(0);
+ },
+ ],
+ ];
+
beforeEach(async () => {
const captureURLResponse = await supertest
.get('/abc/xyz/handshake?one=two three')
@@ -701,76 +724,76 @@ export default function({ getService }: FtrProviderContext) {
)!;
});
- it('should renew session and redirect to the home page if login is for the same user', async () => {
- const samlAuthenticationResponse = await supertest
- .post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', existingSessionCookie.cookieString())
- .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) })
- .expect('location', '/')
- .expect(302);
-
- const newSessionCookie = request.cookie(
- samlAuthenticationResponse.headers['set-cookie'][0]
- )!;
- expect(newSessionCookie.value).to.not.be.empty();
- expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value);
-
- // Tokens from old cookie are invalidated.
- const rejectedResponse = await supertest
- .get('/internal/security/me')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', existingSessionCookie.cookieString())
- .expect(400);
- expect(rejectedResponse.body).to.have.property(
- 'message',
- 'Both access and refresh tokens are expired.'
- );
-
- // Only tokens from new session are valid.
- const acceptedResponse = await supertest
- .get('/internal/security/me')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', newSessionCookie.cookieString())
- .expect(200);
- expect(acceptedResponse.body).to.have.property('username', existingUsername);
- });
-
- it('should create a new session and redirect to the `overwritten_session` if login is for another user', async () => {
- const newUsername = 'c@d.e';
- const samlAuthenticationResponse = await supertest
- .post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', existingSessionCookie.cookieString())
- .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) })
- .expect('location', '/security/overwritten_session')
- .expect(302);
-
- const newSessionCookie = request.cookie(
- samlAuthenticationResponse.headers['set-cookie'][0]
- )!;
- expect(newSessionCookie.value).to.not.be.empty();
- expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value);
-
- // Tokens from old cookie are invalidated.
- const rejectedResponse = await supertest
- .get('/internal/security/me')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', existingSessionCookie.cookieString())
- .expect(400);
- expect(rejectedResponse.body).to.have.property(
- 'message',
- 'Both access and refresh tokens are expired.'
- );
+ for (const [description, setup] of testScenarios) {
+ it(`should renew session and redirect to the home page if login is for the same user ${description}`, async () => {
+ await setup();
+
+ const samlAuthenticationResponse = await supertest
+ .post('/api/security/saml/callback')
+ .set('kbn-xsrf', 'xxx')
+ .set('Cookie', existingSessionCookie.cookieString())
+ .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) })
+ .expect(302);
+
+ expect(samlAuthenticationResponse.headers.location).to.be('/');
+
+ const newSessionCookie = request.cookie(
+ samlAuthenticationResponse.headers['set-cookie'][0]
+ )!;
+ expect(newSessionCookie.value).to.not.be.empty();
+ expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value);
+
+ // Tokens from old cookie are invalidated.
+ const rejectedResponse = await supertest
+ .get('/internal/security/me')
+ .set('kbn-xsrf', 'xxx')
+ .set('Cookie', existingSessionCookie.cookieString())
+ .expect(400);
+ expect(rejectedResponse.body).to.have.property(
+ 'message',
+ 'Both access and refresh tokens are expired.'
+ );
+
+ // Only tokens from new session are valid.
+ await checkSessionCookie(newSessionCookie);
+ });
- // Only tokens from new session are valid.
- const acceptedResponse = await supertest
- .get('/internal/security/me')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', newSessionCookie.cookieString())
- .expect(200);
- expect(acceptedResponse.body).to.have.property('username', newUsername);
- });
+ it(`should create a new session and redirect to the \`overwritten_session\` if login is for another user ${description}`, async () => {
+ await setup();
+
+ const newUsername = 'c@d.e';
+ const samlAuthenticationResponse = await supertest
+ .post('/api/security/saml/callback')
+ .set('kbn-xsrf', 'xxx')
+ .set('Cookie', existingSessionCookie.cookieString())
+ .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) })
+ .expect(302);
+
+ expect(samlAuthenticationResponse.headers.location).to.be(
+ '/security/overwritten_session'
+ );
+
+ const newSessionCookie = request.cookie(
+ samlAuthenticationResponse.headers['set-cookie'][0]
+ )!;
+ expect(newSessionCookie.value).to.not.be.empty();
+ expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value);
+
+ // Tokens from old cookie are invalidated.
+ const rejectedResponse = await supertest
+ .get('/internal/security/me')
+ .set('kbn-xsrf', 'xxx')
+ .set('Cookie', existingSessionCookie.cookieString())
+ .expect(400);
+ expect(rejectedResponse.body).to.have.property(
+ 'message',
+ 'Both access and refresh tokens are expired.'
+ );
+
+ // Only tokens from new session are valid.
+ await checkSessionCookie(newSessionCookie, newUsername);
+ });
+ }
});
describe('handshake with very long URL path or fragment', () => {
diff --git a/yarn.lock b/yarn.lock
index 30747ee555fe2a..a17102b301bb22 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1165,7 +1165,7 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
-"@babel/types@^7.9.5":
+"@babel/types@^7.4", "@babel/types@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==
@@ -1236,10 +1236,10 @@
dependencies:
"@elastic/apm-rum-core" "^5.2.0"
-"@elastic/charts@18.3.0":
- version "18.3.0"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64"
- integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw==
+"@elastic/charts@18.4.1":
+ version "18.4.1"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.1.tgz#19d82c39aef347fd00b33e33b68b683ac4d745b0"
+ integrity sha512-vV5AAKIKbwgY923OD2Rsr77XFHmsUsWWg/aeCZvG5/b6Yb+fNgM0RF94GADiDMvRvQANhTn2CPPVvNfL18MegQ==
dependencies:
classnames "^2.2.6"
d3-array "^1.2.4"
@@ -1377,10 +1377,10 @@
through2 "^2.0.0"
update-notifier "^0.5.0"
-"@elastic/maki@6.2.0":
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489"
- integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg==
+"@elastic/maki@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.3.0.tgz#09780650f1510554bef9121b9db86ce297f021f1"
+ integrity sha512-a2U2DaemIJaW+3nL/sN/+JScdrkoggoGHLDtRPurk2Axnpa9O9QHekmMXLO7eLK1brDpYcplqGE6hwFaMvRRUg==
"@elastic/node-crypto@1.1.1":
version "1.1.1"
@@ -7125,6 +7125,14 @@ babel-plugin-transform-define@^1.3.1:
lodash "^4.17.11"
traverse "0.6.6"
+babel-plugin-transform-imports@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz#9e5f49f751a9d34ba8f4bb988c7e48ed2419c6b6"
+ integrity sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw==
+ dependencies:
+ "@babel/types" "^7.4"
+ is-valid-path "^0.1.1"
+
babel-plugin-transform-inline-consecutive-adds@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1"
@@ -17579,7 +17587,7 @@ is-valid-glob@^1.0.0:
resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=
-is-valid-path@0.1.1:
+is-valid-path@0.1.1, is-valid-path@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df"
integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8=