diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a8dcafeb7753c..92e39c2e634e5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -79,6 +79,7 @@
# Uptime
/x-pack/plugins/uptime @elastic/uptime
+/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime
/x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime
/x-pack/test/functional/apps/uptime @elastic/uptime
/x-pack/test/api_integration/apis/uptime @elastic/uptime
diff --git a/NOTICE.txt b/NOTICE.txt
index 2341a478cbda9..4eec329b7a603 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -261,33 +261,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----
-This product bundles childnode-remove which is available under a
-"MIT" license.
-
-The MIT License (MIT)
-
-Copyright (c) 2016-present, jszhou
-https://github.com/jserz/js_piece
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
---
This product bundles code based on probot-metadata@1.0.0 which is
available under a "MIT" license.
diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc
index c1e727b1eac65..3827cb6e9aa7d 100644
--- a/docs/developer/advanced/upgrading-nodejs.asciidoc
+++ b/docs/developer/advanced/upgrading-nodejs.asciidoc
@@ -14,10 +14,14 @@ Theses files must be updated when upgrading Node.js:
- {kib-repo}blob/{branch}/.node-version[`.node-version`]
- {kib-repo}blob/{branch}/.nvmrc[`.nvmrc`]
- {kib-repo}blob/{branch}/package.json[`package.json`] - The version is specified in the `engines.node` field.
+ - {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property.
+ Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes.
+ These can be found on the https://nodejs.org[nodejs.org] website.
+ Example for Node.js v14.16.1: https://nodejs.org/dist/v14.16.1/SHASUMS256.txt.asc
-See PR {kib-repo}pull/86593[#86593] for an example of how the Node.js version has been upgraded previously.
+See PR {kib-repo}pull/96382[#96382] for an example of how the Node.js version has been upgraded previously.
-In the 6.8 branch, the `.ci/Dockerfile` file does not exist, so when upgrading Node.js in that branch, just skip that file.
+In the 6.8 branch, neither the `.ci/Dockerfile` file nor the `WORKSPACE.bazel` file exists, so when upgrading Node.js in that branch, just skip those files.
=== Backporting
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
new file mode 100644
index 0000000000000..a5694ea2d1af9
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md)
+
+## AutoRefreshDoneFn type
+
+Signature:
+
+```typescript
+export declare type AutoRefreshDoneFn = () => void;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
index 4d75dda61d5c9..521ceeb1e37f2 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
@@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES
| HISTOGRAM | "histogram"
| |
| IP | "ip"
| |
| IP\_RANGE | "ip_range"
| |
+| MISSING | "missing"
| |
| MURMUR3 | "murmur3"
| |
| NESTED | "nested"
| |
| NUMBER | "number"
| |
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 d2e7ef9db05e8..4429f45f55645 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
@@ -47,6 +47,7 @@
| [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | |
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
+| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. |
## Interfaces
@@ -92,6 +93,7 @@
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object |
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
+| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) |
## Variables
@@ -141,6 +143,7 @@
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. |
| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* |
+| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | |
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | |
| [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
new file mode 100644
index 0000000000000..a4b294fb1decd
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+## waitUntilNextSessionCompletes$() function
+
+Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes.
+
+Signature:
+
+```typescript
+export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| sessionService | ISessionService
| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| { waitForIdle } | WaitUntilNextSessionCompletesOptions
| |
+
+Returns:
+
+`import("rxjs").Observable`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
new file mode 100644
index 0000000000000..d575722a22453
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md)
+
+## WaitUntilNextSessionCompletesOptions interface
+
+Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+Signature:
+
+```typescript
+export interface WaitUntilNextSessionCompletesOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | number
| For how long to wait between session state transitions before considering that session completed |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
new file mode 100644
index 0000000000000..60d3df7783852
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md)
+
+## WaitUntilNextSessionCompletesOptions.waitForIdle property
+
+For how long to wait between session state transitions before considering that session completed
+
+Signature:
+
+```typescript
+waitForIdle?: number;
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
index be4c3705bd8de..40fa872ff0fc6 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
@@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES
| HISTOGRAM | "histogram"
| |
| IP | "ip"
| |
| IP\_RANGE | "ip_range"
| |
+| MISSING | "missing"
| |
| MURMUR3 | "murmur3"
| |
| NESTED | "nested"
| |
| NUMBER | "number"
| |
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
index 5201444e69867..290dc10662569 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
@@ -9,9 +9,9 @@ Merges input$ and output$ streams and debounces emit till next macro-task. Could
Signature:
```typescript
-getUpdated$(): Readonly>;
+getUpdated$(): Readonly>;
```
Returns:
-`Readonly>`
+`Readonly>`
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 08cbee8851b98..20bbbcf874c05 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -77,6 +77,13 @@ a|`xpack.actions.`
+
As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates.
+| `xpack.actions.maxResponseContentLength` {ess-icon}
+ | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB).
+
+| `xpack.actions.responseTimeout` {ess-icon}
+ | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as [ms|s|m|h|d|w|M|Y], for example, '20m', '24h', '7d', '1w'. Defaults to 60s.
+
+
|===
[float]
diff --git a/package.json b/package.json
index a1acf73ea26f0..9bddca4665467 100644
--- a/package.json
+++ b/package.json
@@ -131,10 +131,12 @@
"@kbn/crypto": "link:packages/kbn-crypto",
"@kbn/i18n": "link:packages/kbn-i18n",
"@kbn/interpreter": "link:packages/kbn-interpreter",
+ "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils",
"@kbn/legacy-logging": "link:packages/kbn-legacy-logging",
"@kbn/logging": "link:packages/kbn-logging",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
+ "@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
"@kbn/std": "link:packages/kbn-std",
"@kbn/tinymath": "link:packages/kbn-tinymath",
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
@@ -206,7 +208,6 @@
"content-disposition": "0.5.3",
"copy-to-clipboard": "^3.0.8",
"core-js": "^3.6.5",
- "custom-event-polyfill": "^0.3.0",
"cytoscape": "^3.10.0",
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json
index c2e579e7fdbea..80a2255d71805 100644
--- a/packages/kbn-analytics/tsconfig.json
+++ b/packages/kbn-analytics/tsconfig.json
@@ -7,6 +7,7 @@
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true,
+ "isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-analytics/src",
"types": [
diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
index 26b1a6fa2e804..2381faefbff29 100644
--- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
+++ b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
@@ -9,6 +9,7 @@
import Path from 'path';
import globby from 'globby';
+import normalize from 'normalize-path';
import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin';
@@ -32,7 +33,7 @@ export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPa
),
...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)),
])
- );
+ ).map((path) => normalize(path));
const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) =>
// absolute paths returned from globby are using normalize or
diff --git a/packages/kbn-io-ts-utils/jest.config.js b/packages/kbn-io-ts-utils/jest.config.js
new file mode 100644
index 0000000000000..1a71166fae843
--- /dev/null
+++ b/packages/kbn-io-ts-utils/jest.config.js
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-io-ts-utils'],
+};
diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json
new file mode 100644
index 0000000000000..4d6f02d3f85a6
--- /dev/null
+++ b/packages/kbn-io-ts-utils/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@kbn/io-ts-utils",
+ "main": "./target/index.js",
+ "types": "./target/index.d.ts",
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "private": true,
+ "scripts": {
+ "build": "../../node_modules/.bin/tsc",
+ "kbn:bootstrap": "yarn build",
+ "kbn:watch": "yarn build --watch"
+ }
+}
diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts
new file mode 100644
index 0000000000000..2032127b1eb91
--- /dev/null
+++ b/packages/kbn-io-ts-utils/src/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { jsonRt } from './json_rt';
+export { mergeRt } from './merge_rt';
+export { strictKeysRt } from './strict_keys_rt';
diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts
similarity index 85%
rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts
rename to packages/kbn-io-ts-utils/src/json_rt/index.test.ts
index d6c286c672d90..1220639fc7bef 100644
--- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
@@ -12,9 +13,7 @@ import { Right } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { identity } from 'fp-ts/lib/function';
-function getValueOrThrow>(
- either: TEither
-): Right {
+function getValueOrThrow>(either: TEither): Right {
const value = pipe(
either,
fold(() => {
diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts b/packages/kbn-io-ts-utils/src/json_rt/index.ts
similarity index 74%
rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.ts
rename to packages/kbn-io-ts-utils/src/json_rt/index.ts
index 0207145a17be7..bc596d53db54c 100644
--- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts
+++ b/packages/kbn-io-ts-utils/src/json_rt/index.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
similarity index 66%
rename from x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
rename to packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
index af5a0221662d5..b25d4451895f2 100644
--- a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
@@ -1,18 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { isLeft } from 'fp-ts/lib/Either';
-import { merge } from './';
+import { mergeRt } from '.';
import { jsonRt } from '../json_rt';
describe('merge', () => {
it('fails on one or more errors', () => {
- const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]);
+ const type = mergeRt(t.type({ foo: t.string }), t.type({ bar: t.number }));
const result = type.decode({ foo: '' });
@@ -20,10 +21,7 @@ describe('merge', () => {
});
it('merges left to right', () => {
- const typeBoolean = merge([
- t.type({ foo: t.string }),
- t.type({ foo: jsonRt.pipe(t.boolean) }),
- ]);
+ const typeBoolean = mergeRt(t.type({ foo: t.string }), t.type({ foo: jsonRt.pipe(t.boolean) }));
const resultBoolean = typeBoolean.decode({
foo: 'true',
@@ -34,10 +32,7 @@ describe('merge', () => {
foo: true,
});
- const typeString = merge([
- t.type({ foo: jsonRt.pipe(t.boolean) }),
- t.type({ foo: t.string }),
- ]);
+ const typeString = mergeRt(t.type({ foo: jsonRt.pipe(t.boolean) }), t.type({ foo: t.string }));
const resultString = typeString.decode({
foo: 'true',
@@ -50,10 +45,10 @@ describe('merge', () => {
});
it('deeply merges values', () => {
- const type = merge([
+ const type = mergeRt(
t.type({ foo: t.type({ baz: t.string }) }),
- t.type({ foo: t.type({ bar: t.string }) }),
- ]);
+ t.type({ foo: t.type({ bar: t.string }) })
+ );
const result = type.decode({
foo: {
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.ts
similarity index 62%
rename from x-pack/plugins/apm/common/runtime_types/merge/index.ts
rename to packages/kbn-io-ts-utils/src/merge_rt/index.ts
index 451edf678aabe..c582767fb5101 100644
--- a/x-pack/plugins/apm/common/runtime_types/merge/index.ts
+++ b/packages/kbn-io-ts-utils/src/merge_rt/index.ts
@@ -1,31 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { merge as lodashMerge } from 'lodash';
import { isLeft } from 'fp-ts/lib/Either';
-import { ValuesType } from 'utility-types';
-export type MergeType<
- T extends t.Any[],
- U extends ValuesType = ValuesType
-> = t.Type & {
- _tag: 'MergeType';
- types: T;
-};
+type PlainObject = Record;
+
+type DeepMerge = U extends PlainObject
+ ? T extends PlainObject
+ ? Omit &
+ {
+ [key in keyof U]: T extends { [k in key]: any } ? DeepMerge : U[key];
+ }
+ : U
+ : U;
// this is similar to t.intersection, but does a deep merge
// instead of a shallow merge
-export function merge(
- types: [A, B]
-): MergeType<[A, B]>;
+export type MergeType = t.Type<
+ DeepMerge, t.TypeOf>,
+ DeepMerge, t.OutputOf>
+> & {
+ _tag: 'MergeType';
+ types: [T1, T2];
+};
+
+export function mergeRt(a: T1, b: T2): MergeType;
-export function merge(types: t.Any[]) {
+export function mergeRt(...types: t.Any[]) {
const mergeType = new t.Type(
'merge',
(u): u is unknown => {
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
similarity index 77%
rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
index 4212e0430ff5f..ab20ca42a283e 100644
--- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
@@ -14,10 +15,7 @@ describe('strictKeysRt', () => {
it('correctly and deeply validates object keys', () => {
const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [
{
- type: t.intersection([
- t.type({ foo: t.string }),
- t.partial({ bar: t.string }),
- ]),
+ type: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.string })]),
passes: [{ foo: '' }, { foo: '', bar: '' }],
fails: [
{ foo: '', unknownKey: '' },
@@ -26,15 +24,9 @@ describe('strictKeysRt', () => {
},
{
type: t.type({
- path: t.union([
- t.type({ serviceName: t.string }),
- t.type({ transactionType: t.string }),
- ]),
+ path: t.union([t.type({ serviceName: t.string }), t.type({ transactionType: t.string })]),
}),
- passes: [
- { path: { serviceName: '' } },
- { path: { transactionType: '' } },
- ],
+ passes: [{ path: { serviceName: '' } }, { path: { transactionType: '' } }],
fails: [
{ path: { serviceName: '', unknownKey: '' } },
{ path: { transactionType: '', unknownKey: '' } },
@@ -62,9 +54,7 @@ describe('strictKeysRt', () => {
if (!isRight(result)) {
throw new Error(
- `Expected ${JSON.stringify(
- value
- )} to be allowed, but validation failed with ${
+ `Expected ${JSON.stringify(value)} to be allowed, but validation failed with ${
result.left[0].message
}`
);
@@ -76,9 +66,7 @@ describe('strictKeysRt', () => {
if (!isLeft(result)) {
throw new Error(
- `Expected ${JSON.stringify(
- value
- )} to be disallowed, but validation succeeded`
+ `Expected ${JSON.stringify(value)} to be disallowed, but validation succeeded`
);
}
});
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
similarity index 66%
rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
index e90ccf7eb8d31..56afdf54463f7 100644
--- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
+++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
@@ -1,14 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { either, isRight } from 'fp-ts/lib/Either';
import { mapValues, difference, isPlainObject, forEach } from 'lodash';
-import { MergeType, merge } from '../merge';
+import { MergeType, mergeRt } from '../merge_rt';
/*
Type that tracks validated keys, and fails when the input value
@@ -21,7 +22,7 @@ type ParsableType =
| t.PartialType
| t.ExactType
| t.InterfaceType
- | MergeType;
+ | MergeType;
function getKeysInObject>(
object: T,
@@ -32,17 +33,16 @@ function getKeysInObject>(
const ownPrefix = prefix ? `${prefix}.${key}` : key;
keys.push(ownPrefix);
if (isPlainObject(object[key])) {
- keys.push(
- ...getKeysInObject(object[key] as Record, ownPrefix)
- );
+ keys.push(...getKeysInObject(object[key] as Record, ownPrefix));
}
});
return keys;
}
-function addToContextWhenValidated<
- T extends t.InterfaceType | t.PartialType
->(type: T, prefix: string): T {
+function addToContextWhenValidated | t.PartialType>(
+ type: T,
+ prefix: string
+): T {
const validate = (input: unknown, context: t.Context) => {
const result = type.validate(input, context);
const keysType = context[0].type as StrictKeysType;
@@ -50,36 +50,19 @@ function addToContextWhenValidated<
throw new Error('Expected a top-level StrictKeysType');
}
if (isRight(result)) {
- keysType.trackedKeys.push(
- ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)
- );
+ keysType.trackedKeys.push(...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`));
}
return result;
};
if (type._tag === 'InterfaceType') {
- return new t.InterfaceType(
- type.name,
- type.is,
- validate,
- type.encode,
- type.props
- ) as T;
+ return new t.InterfaceType(type.name, type.is, validate, type.encode, type.props) as T;
}
- return new t.PartialType(
- type.name,
- type.is,
- validate,
- type.encode,
- type.props
- ) as T;
+ return new t.PartialType(type.name, type.is, validate, type.encode, type.props) as T;
}
-function trackKeysOfValidatedTypes(
- type: ParsableType | t.Any,
- prefix: string = ''
-): t.Any {
+function trackKeysOfValidatedTypes(type: ParsableType | t.Any, prefix: string = ''): t.Any {
if (!('_tag' in type)) {
return type;
}
@@ -89,27 +72,24 @@ function trackKeysOfValidatedTypes(
case 'IntersectionType': {
const collectionType = type as t.IntersectionType;
return t.intersection(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
);
}
case 'UnionType': {
const collectionType = type as t.UnionType;
return t.union(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
);
}
case 'MergeType': {
- const collectionType = type as MergeType;
- return merge(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ const collectionType = type as MergeType;
+ return mergeRt(
+ ...(collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [
+ t.Any,
+ t.Any
+ ])
);
}
@@ -142,9 +122,7 @@ function trackKeysOfValidatedTypes(
case 'ExactType': {
const exactType = type as t.ExactType;
- return t.exact(
- trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps
- );
+ return t.exact(trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps);
}
default:
@@ -169,17 +147,11 @@ class StrictKeysType<
(input, context) => {
this.trackedKeys.length = 0;
return either.chain(trackedType.validate(input, context), (i) => {
- const originalKeys = getKeysInObject(
- input as Record
- );
+ const originalKeys = getKeysInObject(input as Record);
const excessKeys = difference(originalKeys, this.trackedKeys);
if (excessKeys.length) {
- return t.failure(
- i,
- context,
- `Excess keys are not allowed: \n${excessKeys.join('\n')}`
- );
+ return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`);
}
return t.success(i);
diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json
new file mode 100644
index 0000000000000..6c67518e21073
--- /dev/null
+++ b/packages/kbn-io-ts-utils/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "incremental": false,
+ "outDir": "./target",
+ "stripInternal": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-io-ts-utils/src",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "./src/**/*.ts"
+ ]
+}
diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md
new file mode 100644
index 0000000000000..e22205540ef31
--- /dev/null
+++ b/packages/kbn-server-route-repository/README.md
@@ -0,0 +1,7 @@
+# @kbn/server-route-repository
+
+Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition.
+
+## Usage
+
+TBD
diff --git a/packages/kbn-server-route-repository/jest.config.js b/packages/kbn-server-route-repository/jest.config.js
new file mode 100644
index 0000000000000..7449bb7cd3860
--- /dev/null
+++ b/packages/kbn-server-route-repository/jest.config.js
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-server-route-repository'],
+};
diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json
new file mode 100644
index 0000000000000..ce1ca02d0c4f6
--- /dev/null
+++ b/packages/kbn-server-route-repository/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@kbn/server-route-repository",
+ "main": "./target/index.js",
+ "types": "./target/index.d.ts",
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "private": true,
+ "scripts": {
+ "build": "../../node_modules/.bin/tsc",
+ "kbn:bootstrap": "yarn build",
+ "kbn:watch": "yarn build --watch"
+ },
+ "dependencies": {
+ "@kbn/io-ts-utils": "link:../kbn-io-ts-utils"
+ }
+}
diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts
new file mode 100644
index 0000000000000..edf9bd657f995
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/create_server_route_factory.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import {
+ ServerRouteCreateOptions,
+ ServerRouteHandlerResources,
+ RouteParamsRT,
+ ServerRoute,
+} from './typings';
+
+export function createServerRouteFactory<
+ TRouteHandlerResources extends ServerRouteHandlerResources,
+ TRouteCreateOptions extends ServerRouteCreateOptions
+>(): <
+ TEndpoint extends string,
+ TReturnType,
+ TRouteParamsRT extends RouteParamsRT | undefined = undefined
+>(
+ route: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >
+) => ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+> {
+ return (route) => route;
+}
diff --git a/packages/kbn-server-route-repository/src/create_server_route_repository.ts b/packages/kbn-server-route-repository/src/create_server_route_repository.ts
new file mode 100644
index 0000000000000..5ac89ebcac77f
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/create_server_route_repository.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import {
+ ServerRouteHandlerResources,
+ ServerRouteRepository,
+ ServerRouteCreateOptions,
+} from './typings';
+
+export function createServerRouteRepository<
+ TRouteHandlerResources extends ServerRouteHandlerResources = never,
+ TRouteCreateOptions extends ServerRouteCreateOptions = never
+>(): ServerRouteRepository {
+ let routes: Record = {};
+
+ return {
+ add(route) {
+ routes = {
+ ...routes,
+ [route.endpoint]: route,
+ };
+
+ return this as any;
+ },
+ merge(repository) {
+ routes = {
+ ...routes,
+ ...Object.fromEntries(repository.getRoutes().map((route) => [route.endpoint, route])),
+ };
+
+ return this as any;
+ },
+ getRoutes: () => Object.values(routes),
+ };
+}
diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts
new file mode 100644
index 0000000000000..08ef303ad0b3a
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { jsonRt } from '@kbn/io-ts-utils';
+import * as t from 'io-ts';
+import { decodeRequestParams } from './decode_request_params';
+
+describe('decodeRequestParams', () => {
+ it('decodes request params', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {
+ serviceName: 'opbeans-java',
+ },
+ body: null,
+ query: {
+ start: '',
+ },
+ },
+ t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.type({
+ start: t.string,
+ }),
+ })
+ );
+ };
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({
+ path: {
+ serviceName: 'opbeans-java',
+ },
+ query: {
+ start: '',
+ },
+ });
+ });
+
+ it('fails on excess keys', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {
+ serviceName: 'opbeans-java',
+ extraKey: '',
+ },
+ body: null,
+ query: {
+ start: '',
+ },
+ },
+ t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.type({
+ start: t.string,
+ }),
+ })
+ );
+ };
+
+ expect(decode).toThrowErrorMatchingInlineSnapshot(`
+ "Excess keys are not allowed:
+ path.extraKey"
+ `);
+ });
+
+ it('returns the decoded output', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {},
+ query: {
+ _inspect: 'true',
+ },
+ body: null,
+ },
+ t.type({
+ query: t.type({
+ _inspect: jsonRt.pipe(t.boolean),
+ }),
+ })
+ );
+ };
+
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({
+ query: {
+ _inspect: true,
+ },
+ });
+ });
+
+ it('strips empty params', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {},
+ query: {},
+ body: {},
+ },
+ t.type({
+ body: t.any,
+ })
+ );
+ };
+
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({});
+ });
+});
diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts
new file mode 100644
index 0000000000000..00492d69b8ac5
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/decode_request_params.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import * as t from 'io-ts';
+import { omitBy, isPlainObject, isEmpty } from 'lodash';
+import { isLeft } from 'fp-ts/lib/Either';
+import { PathReporter } from 'io-ts/lib/PathReporter';
+import Boom from '@hapi/boom';
+import { strictKeysRt } from '@kbn/io-ts-utils';
+import { RouteParamsRT } from './typings';
+
+interface KibanaRequestParams {
+ body: unknown;
+ query: unknown;
+ params: unknown;
+}
+
+export function decodeRequestParams(
+ params: KibanaRequestParams,
+ paramsRt: T
+): t.OutputOf {
+ const paramMap = omitBy(
+ {
+ path: params.params,
+ body: params.body,
+ query: params.query,
+ },
+ (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val))
+ );
+
+ // decode = validate
+ const result = strictKeysRt(paramsRt).decode(paramMap);
+
+ if (isLeft(result)) {
+ throw Boom.badRequest(PathReporter.report(result)[0]);
+ }
+
+ return result.right;
+}
diff --git a/packages/kbn-server-route-repository/src/format_request.ts b/packages/kbn-server-route-repository/src/format_request.ts
new file mode 100644
index 0000000000000..49004a78ce0e0
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/format_request.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { parseEndpoint } from './parse_endpoint';
+
+export function formatRequest(endpoint: string, pathParams: Record = {}) {
+ const { method, pathname: rawPathname } = parseEndpoint(endpoint);
+
+ // replace template variables with path params
+ const pathname = Object.keys(pathParams).reduce((acc, paramName) => {
+ return acc.replace(`{${paramName}}`, pathParams[paramName]);
+ }, rawPathname);
+
+ return { method, pathname };
+}
diff --git a/packages/kbn-server-route-repository/src/index.ts b/packages/kbn-server-route-repository/src/index.ts
new file mode 100644
index 0000000000000..23621c5b213bc
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { createServerRouteRepository } from './create_server_route_repository';
+export { createServerRouteFactory } from './create_server_route_factory';
+export { formatRequest } from './format_request';
+export { parseEndpoint } from './parse_endpoint';
+export { decodeRequestParams } from './decode_request_params';
+export { routeValidationObject } from './route_validation_object';
+export {
+ RouteRepositoryClient,
+ ReturnOf,
+ EndpointOf,
+ ClientRequestParamsOf,
+ DecodedRequestParamsOf,
+ ServerRouteRepository,
+ ServerRoute,
+ RouteParamsRT,
+} from './typings';
diff --git a/packages/kbn-server-route-repository/src/parse_endpoint.ts b/packages/kbn-server-route-repository/src/parse_endpoint.ts
new file mode 100644
index 0000000000000..fd40489b0f4a5
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/parse_endpoint.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+type Method = 'get' | 'post' | 'put' | 'delete';
+
+export function parseEndpoint(endpoint: string) {
+ const parts = endpoint.split(' ');
+
+ const method = parts[0].trim().toLowerCase() as Method;
+ const pathname = parts[1].trim();
+
+ if (!['get', 'post', 'put', 'delete'].includes(method)) {
+ throw new Error('Endpoint was not prefixed with a valid HTTP method');
+ }
+
+ return { method, pathname };
+}
diff --git a/packages/kbn-server-route-repository/src/route_validation_object.ts b/packages/kbn-server-route-repository/src/route_validation_object.ts
new file mode 100644
index 0000000000000..550be8d20d446
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/route_validation_object.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { schema } from '@kbn/config-schema';
+
+const anyObject = schema.object({}, { unknowns: 'allow' });
+
+export const routeValidationObject = {
+ // `body` can be null, but `validate` expects non-nullable types
+ // if any validation is defined. Not having validation currently
+ // means we don't get the payload. See
+ // https://github.com/elastic/kibana/issues/50179
+ body: schema.nullable(anyObject),
+ params: anyObject,
+ query: anyObject,
+};
diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts
new file mode 100644
index 0000000000000..c9015e19b82f8
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/test_types.ts
@@ -0,0 +1,238 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import * as t from 'io-ts';
+import { createServerRouteRepository } from './create_server_route_repository';
+import { decodeRequestParams } from './decode_request_params';
+import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings';
+
+function assertType(value: TShape) {
+ return value;
+}
+
+// Generic arguments for createServerRouteRepository should be set,
+// if not, registering routes should not be allowed
+createServerRouteRepository().add({
+ // @ts-expect-error
+ endpoint: 'any_endpoint',
+ // @ts-expect-error
+ handler: async ({ params }) => {},
+});
+
+// If a params codec is not set, its type should not be available in
+// the request handler.
+createServerRouteRepository<{}, {}>().add({
+ endpoint: 'endpoint_without_params',
+ handler: async (resources) => {
+ // @ts-expect-error Argument of type '{}' is not assignable to parameter of type '{ params: any; }'.
+ assertType<{ params: any }>(resources);
+ },
+});
+
+// If a params codec is set, its type _should_ be available in the
+// request handler.
+createServerRouteRepository<{}, {}>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async (resources) => {
+ assertType<{ params: { path: { serviceName: string } } }>(resources);
+ },
+});
+
+// Resources should be passed to the request handler.
+createServerRouteRepository<{ context: { getSpaceId: () => string } }, {}>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async ({ context }) => {
+ const spaceId = context.getSpaceId();
+ assertType(spaceId);
+ },
+});
+
+// Create options are available when registering a route.
+createServerRouteRepository<{}, { options: { tags: string[] } }>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ options: {
+ tags: [],
+ },
+ handler: async (resources) => {
+ assertType<{ params: { path: { serviceName: string } } }>(resources);
+ },
+});
+
+const repository = createServerRouteRepository<{}, {}>()
+ .add({
+ endpoint: 'endpoint_without_params',
+ handler: async () => {
+ return {
+ noParamsForMe: true,
+ };
+ },
+ })
+ .add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async () => {
+ return {
+ yesParamsForMe: true,
+ };
+ },
+ })
+ .add({
+ endpoint: 'endpoint_with_optional_params',
+ params: t.partial({
+ query: t.partial({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async () => {
+ return {
+ someParamsForMe: true,
+ };
+ },
+ });
+
+type TestRepository = typeof repository;
+
+// EndpointOf should return all valid endpoints of a repository
+
+assertType>>([
+ 'endpoint_with_params',
+ 'endpoint_without_params',
+ 'endpoint_with_optional_params',
+]);
+
+// @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"'
+assertType>>(['this_endpoint_does_not_exist']);
+
+// ReturnOf should return the return type of a request handler.
+
+assertType>({
+ noParamsForMe: true,
+});
+
+const noParamsInvalid: ReturnOf = {
+ // @ts-expect-error type '{ paramsForMe: boolean; }' is not assignable to type '{ noParamsForMe: boolean; }'.
+ paramsForMe: true,
+};
+
+// RouteRepositoryClient
+
+type TestClient = RouteRepositoryClient;
+
+const client: TestClient = {} as any;
+
+// It should respect any additional create options.
+
+// @ts-expect-error Property 'timeout' is missing
+client({
+ endpoint: 'endpoint_without_params',
+});
+
+client({
+ endpoint: 'endpoint_without_params',
+ timeout: 1,
+});
+
+// It does not allow params for routes without a params codec
+client({
+ endpoint: 'endpoint_without_params',
+ // @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type
+ params: {},
+ timeout: 1,
+});
+
+// It requires params for routes with a params codec
+client({
+ endpoint: 'endpoint_with_params',
+ params: {
+ // @ts-expect-error property 'serviceName' is missing in type '{}'
+ path: {},
+ },
+ timeout: 1,
+});
+
+// Params are optional if the codec has no required keys
+client({
+ endpoint: 'endpoint_with_optional_params',
+ timeout: 1,
+});
+
+// If optional, an error will still occur if the params do not match
+client({
+ endpoint: 'endpoint_with_optional_params',
+ timeout: 1,
+ params: {
+ // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type
+ path: '',
+ },
+});
+
+// The return type is correctly inferred
+client({
+ endpoint: 'endpoint_with_params',
+ params: {
+ path: {
+ serviceName: '',
+ },
+ },
+ timeout: 1,
+}).then((res) => {
+ assertType<{
+ noParamsForMe: boolean;
+ // @ts-expect-error Property 'noParamsForMe' is missing in type
+ }>(res);
+
+ assertType<{
+ yesParamsForMe: boolean;
+ }>(res);
+});
+
+// decodeRequestParams should return the type of the codec that is passed
+assertType<{ path: { serviceName: string } }>(
+ decodeRequestParams(
+ {
+ params: {
+ serviceName: 'serviceName',
+ },
+ body: undefined,
+ query: undefined,
+ },
+ t.type({ path: t.type({ serviceName: t.string }) })
+ )
+);
+
+assertType<{ path: { serviceName: boolean } }>(
+ // @ts-expect-error The types of 'path.serviceName' are incompatible between these types.
+ decodeRequestParams(
+ {
+ params: {
+ serviceName: 'serviceName',
+ },
+ body: undefined,
+ query: undefined,
+ },
+ t.type({ path: t.type({ serviceName: t.string }) })
+ )
+);
diff --git a/packages/kbn-server-route-repository/src/typings.ts b/packages/kbn-server-route-repository/src/typings.ts
new file mode 100644
index 0000000000000..c27f67c71e88b
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/typings.ts
@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as t from 'io-ts';
+import { RequiredKeys } from 'utility-types';
+
+type MaybeOptional }> = RequiredKeys<
+ T['params']
+> extends never
+ ? { params?: T['params'] }
+ : { params: T['params'] };
+
+type WithoutIncompatibleMethods = Omit & {
+ encode: t.Encode;
+ asEncoder: () => t.Encoder;
+};
+
+export type RouteParamsRT = WithoutIncompatibleMethods<
+ t.Type<{
+ path?: any;
+ query?: any;
+ body?: any;
+ }>
+>;
+
+export interface RouteState {
+ [endpoint: string]: ServerRoute;
+}
+
+export type ServerRouteHandlerResources = Record;
+export type ServerRouteCreateOptions = Record;
+
+export type ServerRoute<
+ TEndpoint extends string,
+ TRouteParamsRT extends RouteParamsRT | undefined,
+ TRouteHandlerResources extends ServerRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions extends ServerRouteCreateOptions
+> = {
+ endpoint: TEndpoint;
+ params?: TRouteParamsRT;
+ handler: ({}: TRouteHandlerResources &
+ (TRouteParamsRT extends RouteParamsRT
+ ? DecodedRequestParamsOfType
+ : {})) => Promise;
+} & TRouteCreateOptions;
+
+export interface ServerRouteRepository<
+ TRouteHandlerResources extends ServerRouteHandlerResources = ServerRouteHandlerResources,
+ TRouteCreateOptions extends ServerRouteCreateOptions = ServerRouteCreateOptions,
+ TRouteState extends RouteState = RouteState
+> {
+ add<
+ TEndpoint extends string,
+ TReturnType,
+ TRouteParamsRT extends RouteParamsRT | undefined = undefined
+ >(
+ route: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >
+ ): ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ TRouteState &
+ {
+ [key in TEndpoint]: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >;
+ }
+ >;
+ merge<
+ TServerRouteRepository extends ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions
+ >
+ >(
+ repository: TServerRouteRepository
+ ): TServerRouteRepository extends ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ infer TRouteStateToMerge
+ >
+ ? ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ TRouteState & TRouteStateToMerge
+ >
+ : never;
+ getRoutes: () => Array<
+ ServerRoute
+ >;
+}
+
+type ClientRequestParamsOfType<
+ TRouteParamsRT extends RouteParamsRT
+> = TRouteParamsRT extends t.Mixed
+ ? MaybeOptional<{
+ params: t.OutputOf;
+ }>
+ : {};
+
+type DecodedRequestParamsOfType<
+ TRouteParamsRT extends RouteParamsRT
+> = TRouteParamsRT extends t.Mixed
+ ? MaybeOptional<{
+ params: t.TypeOf;
+ }>
+ : {};
+
+export type EndpointOf<
+ TServerRouteRepository extends ServerRouteRepository
+> = TServerRouteRepository extends ServerRouteRepository
+ ? keyof TRouteState
+ : never;
+
+export type ReturnOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ any,
+ any,
+ infer TReturnType,
+ ServerRouteCreateOptions
+ >
+ ? TReturnType
+ : never
+ : never
+ : never;
+
+export type DecodedRequestParamsOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ infer TRouteParamsRT,
+ any,
+ any,
+ ServerRouteCreateOptions
+ >
+ ? TRouteParamsRT extends RouteParamsRT
+ ? DecodedRequestParamsOfType
+ : {}
+ : never
+ : never
+ : never;
+
+export type ClientRequestParamsOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ infer TRouteParamsRT,
+ any,
+ any,
+ ServerRouteCreateOptions
+ >
+ ? TRouteParamsRT extends RouteParamsRT
+ ? ClientRequestParamsOfType
+ : {}
+ : never
+ : never
+ : never;
+
+export type RouteRepositoryClient<
+ TServerRouteRepository extends ServerRouteRepository,
+ TAdditionalClientOptions extends Record
+> = >(
+ options: {
+ endpoint: TEndpoint;
+ } & ClientRequestParamsOf &
+ TAdditionalClientOptions
+) => Promise>;
diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json
new file mode 100644
index 0000000000000..8f1e72172c675
--- /dev/null
+++ b/packages/kbn-server-route-repository/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "incremental": false,
+ "outDir": "./target",
+ "stripInternal": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-server-route-repository/src",
+ "types": [
+ "jest",
+ "node"
+ ],
+ "noUnusedLocals": false
+ },
+ "include": [
+ "./src/**/*.ts"
+ ]
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
index 5d946b73d9759..f55a9aa80d40d 100644
--- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
@@ -7,7 +7,9 @@
*/
export { ErrorReporter } from './error_reporter';
-export { TaskContext, createTaskContext } from './task_context';
+
+export type { TaskContext } from './task_context';
+export { createTaskContext } from './task_context';
export { parseConfigsTask } from './parse_configs_task';
export { extractCollectorsTask } from './extract_collectors_task';
diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json
index 39946fe9907e5..419af1d02f83b 100644
--- a/packages/kbn-telemetry-tools/tsconfig.json
+++ b/packages/kbn-telemetry-tools/tsconfig.json
@@ -6,7 +6,8 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
- "sourceRoot": "../../../../packages/kbn-telemetry-tools/src"
+ "sourceRoot": "../../../../packages/kbn-telemetry-tools/src",
+ "isolatedModules": true
},
"include": [
"src/**/*",
diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
index 8ef11e2dba462..63eca93def64d 100644
--- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
+++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
@@ -11,6 +11,7 @@ import Path from 'path';
import { REPO_ROOT } from '@kbn/utils';
import { run, createFailError, createFlagError } from '@kbn/dev-utils';
import globby from 'globby';
+import normalize from 'normalize-path';
import { getFailures, TestFailure } from './get_failures';
import { GithubApi, GithubIssueMini } from './github_api';
@@ -61,7 +62,9 @@ export function runFailedTestsReporterCli() {
throw createFlagError('Missing --build-url or process.env.BUILD_URL');
}
- const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS;
+ const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) =>
+ normalize(Path.resolve(p))
+ );
log.info('Searching for reports at', patterns);
const reportPaths = await globby(patterns, {
absolute: true,
diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js
index abbf911cfc8fc..a9ec32023f2bf 100644
--- a/packages/kbn-ui-shared-deps/polyfills.js
+++ b/packages/kbn-ui-shared-deps/polyfills.js
@@ -8,7 +8,6 @@
require('core-js/stable');
require('regenerator-runtime/runtime');
-require('custom-event-polyfill');
if (typeof window.Event === 'object') {
// IE11 doesn't support unknown event types, required by react-use
@@ -17,6 +16,4 @@ if (typeof window.Event === 'object') {
}
require('whatwg-fetch');
-require('abortcontroller-polyfill/dist/polyfill-patch-fetch');
-require('./vendor/childnode_remove_polyfill');
require('symbol-observable');
diff --git a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
deleted file mode 100644
index d8818fe809ccb..0000000000000
--- a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable @kbn/eslint/require-license-header */
-
-/* @notice
- * This product bundles childnode-remove which is available under a
- * "MIT" license.
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2016-present, jszhou
- * https://github.com/jserz/js_piece
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-/* eslint-disable */
-
-(function (arr) {
- arr.forEach(function (item) {
- if (item.hasOwnProperty('remove')) {
- return;
- }
- Object.defineProperty(item, 'remove', {
- configurable: true,
- enumerable: true,
- writable: true,
- value: function remove() {
- if (this.parentNode !== null)
- this.parentNode.removeChild(this);
- }
- });
- });
-})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index e0fd649a43df7..6cc94208fbcce 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -166,6 +166,8 @@ kibana_vars=(
xpack.actions.proxyBypassHosts
xpack.actions.proxyOnlyHosts
xpack.actions.rejectUnauthorized
+ xpack.actions.maxResponseContentLength
+ xpack.actions.responseTimeout
xpack.alerts.healthCheck.interval
xpack.alerts.invalidateApiKeysTask.interval
xpack.alerts.invalidateApiKeysTask.removalDelay
diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts
index 3a296ec76f3e6..004e17b87ac8b 100644
--- a/src/dev/build/tasks/package_json/find_used_dependencies.ts
+++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts
@@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
+import Path from 'path';
import globby from 'globby';
+import normalize from 'normalize-path';
// @ts-ignore
import { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser';
@@ -21,16 +23,16 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir:
// Define the entry points for the server code in order to
// start here later looking for the server side dependencies
const mainCodeEntries = [
- `${baseDir}/src/cli/dist.js`,
- `${baseDir}/src/cli_keystore/dist.js`,
- `${baseDir}/src/cli_plugin/dist.js`,
+ Path.resolve(baseDir, `src/cli/dist.js`),
+ Path.resolve(baseDir, `src/cli_keystore/dist.js`),
+ Path.resolve(baseDir, `src/cli_plugin/dist.js`),
];
const discoveredPluginEntries = await globby([
- `${baseDir}/src/plugins/*/server/index.js`,
- `!${baseDir}/src/plugins/**/public`,
- `${baseDir}/x-pack/plugins/*/server/index.js`,
- `!${baseDir}/x-pack/plugins/**/public`,
+ normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)),
+ `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`,
+ normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)),
+ `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`,
]);
// It will include entries that cannot be discovered
@@ -40,7 +42,7 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir:
// Another way would be to include an index file and import all the functions
// using named imports
const dynamicRequiredEntries = await globby([
- `${baseDir}/src/plugins/vis_type_timelion/server/**/*.js`,
+ normalize(Path.resolve(baseDir, 'src/plugins/vis_type_timelion/server/**/*.js')),
]);
// Compose all the needed entries
diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
index 2bc75785ee6a7..7347529239176 100644
--- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
+++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
@@ -12,6 +12,7 @@ import Fs from 'fs';
import del from 'del';
import cpy from 'cpy';
import globby from 'globby';
+import normalize from 'normalize-path';
import {
ToolingLog,
createAbsolutePathSerializer,
@@ -98,7 +99,10 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and
const files = Object.fromEntries(
globby
- .sync(outDirs, { dot: true })
+ .sync(
+ outDirs.map((p) => normalize(p)),
+ { dot: true }
+ )
.map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')])
);
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 3d6f08f321977..e7e2ccfd46b9c 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -10,7 +10,7 @@ import { History } from 'history';
import { merge, Subject, Subscription } from 'rxjs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { debounceTime, tap } from 'rxjs/operators';
+import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators';
import { useKibana } from '../../../kibana_react/public';
import { DashboardConstants } from '../dashboard_constants';
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
@@ -30,7 +30,7 @@ import {
useSavedDashboard,
} from './hooks';
-import { IndexPattern } from '../services/data';
+import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data';
import { EmbeddableRenderer } from '../services/embeddable';
import { DashboardContainerInput } from '.';
import { leaveConfirmStrings } from '../dashboard_strings';
@@ -209,14 +209,26 @@ export function DashboardApp({
);
subscriptions.add(
- merge(
- data.query.timefilter.timefilter.getAutoRefreshFetch$(),
- searchSessionIdQuery$
- ).subscribe(() => {
+ searchSessionIdQuery$.subscribe(() => {
triggerRefresh$.next({ force: true });
})
);
+ subscriptions.add(
+ data.query.timefilter.timefilter
+ .getAutoRefreshFetch$()
+ .pipe(
+ tap(() => {
+ triggerRefresh$.next({ force: true });
+ }),
+ switchMap((done) =>
+ // best way on a dashboard to estimate that panels are updated is to rely on search session service state
+ waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
+ )
+ )
+ .subscribe()
+ );
+
dashboardStateManager.registerChangeListener(() => {
setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter));
// we aren't checking dirty state because there are changes the container needs to know about
diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx
index 60e74a3fa126c..30006e2b497bd 100644
--- a/src/plugins/data/README.mdx
+++ b/src/plugins/data/README.mdx
@@ -5,7 +5,7 @@ title: Data services
image: https://source.unsplash.com/400x175/?Search
summary: The data plugin contains services for searching, querying and filtering.
date: 2020-12-02
-tags: ['kibana','dev', 'contributor', 'api docs']
+tags: ['kibana', 'dev', 'contributor', 'api docs']
---
# data
@@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints:
- Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
- Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
-
### Index Patterns API
Index Patterns REST API allows you to create, retrieve and delete index patterns. I also
@@ -212,11 +211,10 @@ The endpoint returns the created index pattern object.
```json
{
- "index_pattern": {}
+ "index_pattern": {}
}
```
-
#### Fetch an index pattern by ID
Retrieve an index pattern by its ID.
@@ -229,23 +227,22 @@ Returns an index pattern object.
```json
{
- "index_pattern": {
- "id": "...",
- "version": "...",
- "title": "...",
- "type": "...",
- "intervalName": "...",
- "timeFieldName": "...",
- "sourceFilters": [],
- "fields": {},
- "typeMeta": {},
- "fieldFormats": {},
- "fieldAttrs": {}
- }
+ "index_pattern": {
+ "id": "...",
+ "version": "...",
+ "title": "...",
+ "type": "...",
+ "intervalName": "...",
+ "timeFieldName": "...",
+ "sourceFilters": [],
+ "fields": {},
+ "typeMeta": {},
+ "fieldFormats": {},
+ "fieldAttrs": {}
+ }
}
```
-
#### Delete an index pattern by ID
Delete and index pattern by its ID.
@@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Returns an '200 OK` response with empty body on success.
-
#### Partially update an index pattern by ID
Update part of an index pattern. Only provided fields will be updated on the
index pattern, missing fields will stay as they are persisted.
These fields can be update partially:
- - `title`
- - `timeFieldName`
- - `intervalName`
- - `fields` (optionally refresh fields)
- - `sourceFilters`
- - `fieldFormatMap`
- - `type`
- - `typeMeta`
+
+- `title`
+- `timeFieldName`
+- `intervalName`
+- `fields` (optionally refresh fields)
+- `sourceFilters`
+- `fieldFormatMap`
+- `type`
+- `typeMeta`
Update a title of an index pattern.
@@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Fields API
Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`.
-
#### Update fields
Update endpoint allows you to update fields presentation metadata, such as `count`,
@@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Scripted Fields API
Scripted Fields API provides CRUD API for scripted fields of an index pattern.
@@ -487,7 +477,7 @@ Returns the field object.
```json
{
- "field": {}
+ "field": {}
}
```
@@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri
}
```
-
## Query
The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries.
It contains sub-services for each of those configurations:
- - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
- - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
- - `data.query.queryString` - Responsible for the query string and query language settings.
- - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
+- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
+- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
+- `data.query.queryString` - Responsible for the query string and query language settings.
+- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- A simple use case is:
+Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
- ```.ts
- function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
- data.query.state$.subscribe(() => {
+A simple use case is:
- // Constuct the query portion of the search request
- const query = data.query.getEsQuery(indexPattern);
+```.ts
+function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
+ data.query.state$.subscribe(() => {
+
+ // Constuct the query portion of the search request
+ const query = data.query.getEsQuery(indexPattern);
+
+ // Construct a request
+ const request = {
+ params: {
+ index: indexPattern.title,
+ body: {
+ aggs: aggConfigs.toDsl(),
+ query,
+ },
+ },
+ };
+
+ // Search with the `data.query` config
+ const search$ = data.search.search(request);
+
+ ...
+ });
+}
- // Construct a request
- const request = {
- params: {
- index: indexPattern.title,
- body: {
- aggs: aggConfigs.toDsl(),
- query,
- },
- },
- };
+```
- // Search with the `data.query` config
- const search$ = data.search.search(request);
+### Timefilter
- ...
- });
- }
+`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings.
+
+#### Autorefresh
- ```
+Timefilter provides an API for setting and getting current auto refresh state:
+
+```ts
+const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval();
+
+data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval
+```
+
+Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified
+when it is time to refresh data because of auto refresh.
+This API expects apps to confirm when they are done with reloading the data.
+The confirmation mechanism is needed to prevent excessive queue of fetches.
+
+```
+import { refetchData } from '../my-app'
+
+const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$()
+autoRefreshFetch$.subscribe((done) => {
+ try {
+ await refetchData();
+ } finally {
+ // confirm that data fetching was finished
+ done();
+ }
+})
+
+function unmount() {
+ // don't forget to unsubscribe when leaving the app
+ autoRefreshFetch$.unsubscribe()
+}
+
+```
## Search
diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts
index c46e5c5266f55..e6f815e058ce3 100644
--- a/src/plugins/data/common/kbn_field_types/types.ts
+++ b/src/plugins/data/common/kbn_field_types/types.ts
@@ -80,4 +80,5 @@ export enum KBN_FIELD_TYPES {
OBJECT = 'object',
NESTED = 'nested',
HISTOGRAM = 'histogram',
+ MISSING = 'missing',
}
diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts
index 297af560081b1..3ce528e6ed893 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.test.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts
@@ -230,7 +230,7 @@ describe('AggConfigs', () => {
describe('#toDsl', () => {
beforeEach(() => {
indexPattern = stubIndexPattern as IndexPattern;
- indexPattern.fields.getByName = (name) => (name as unknown) as IndexPatternField;
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
});
it('uses the sorted aggs', () => {
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
index 4e278d5872a3e..56e720d237c45 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
@@ -16,16 +16,33 @@ import { AggConfigs, CreateAggConfigParams } from '../agg_configs';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketAggConfig } from './bucket_agg_type';
import { mockAggTypesRegistry } from '../test_helpers';
+import type { IndexPatternField } from '../../../index_patterns';
+import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern';
const indexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
- name: 'field',
+ name: 'machine.os.raw',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'geo.src',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
},
],
-} as any;
+} as IndexPattern;
+
+indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
const singleTerm = {
aggs: [
diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts
index bb34d7ede453c..09dfbb28a4e53 100644
--- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts
@@ -10,6 +10,8 @@ import { AggConfigs } from '../agg_configs';
import { METRIC_TYPES } from '../metrics';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
+import type { IndexPatternField } from '../../../index_patterns';
+import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern';
describe('Terms Agg', () => {
describe('order agg editor UI', () => {
@@ -17,16 +19,44 @@ describe('Terms Agg', () => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
- fields: {
- getByName: () => field,
- filter: () => [field],
- },
- } as any;
+ fields: [
+ {
+ name: 'field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'string_field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'empty_number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ ],
+ } as IndexPattern;
- const field = {
- name: 'field',
- indexPattern,
- };
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
+ indexPattern.fields.filter = () => indexPattern.fields;
return new AggConfigs(
indexPattern,
@@ -207,16 +237,28 @@ describe('Terms Agg', () => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
- fields: {
- getByName: () => field,
- filter: () => [field],
- },
- } as any;
+ fields: [
+ {
+ name: 'string_field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ ],
+ } as IndexPattern;
- const field = {
- name: 'field',
- indexPattern,
- };
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
+ indexPattern.fields.filter = () => indexPattern.fields;
const aggConfigs = new AggConfigs(
indexPattern,
diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts
index 2d3ff8f5fdba8..62dac9831211a 100644
--- a/src/plugins/data/common/search/aggs/param_types/field.ts
+++ b/src/plugins/data/common/search/aggs/param_types/field.ts
@@ -8,7 +8,10 @@
import { i18n } from '@kbn/i18n';
import { IAggConfig } from '../agg_config';
-import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common';
+import {
+ SavedFieldNotFound,
+ SavedFieldTypeInvalidForAgg,
+} from '../../../../../../plugins/kibana_utils/common';
import { BaseParamType } from './base';
import { propFilter } from '../utils';
import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types';
@@ -47,13 +50,49 @@ export class FieldParamType extends BaseParamType {
);
}
- if (field.scripted) {
+ if (field.type === KBN_FIELD_TYPES.MISSING) {
+ throw new SavedFieldNotFound(
+ i18n.translate(
+ 'data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage',
+ {
+ defaultMessage:
+ 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.',
+ values: {
+ fieldParameter: field.name,
+ },
+ }
+ )
+ );
+ }
+
+ const validField = this.getAvailableFields(aggConfig).find(
+ (f: any) => f.name === field.name
+ );
+
+ if (!validField) {
+ throw new SavedFieldTypeInvalidForAgg(
+ i18n.translate(
+ 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage',
+ {
+ defaultMessage:
+ 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.',
+ values: {
+ fieldParameter: field.name,
+ aggType: aggConfig?.type?.title,
+ indexPatternTitle: aggConfig.getIndexPattern().title,
+ },
+ }
+ )
+ );
+ }
+
+ if (validField.scripted) {
output.params.script = {
- source: field.script,
- lang: field.lang,
+ source: validField.script,
+ lang: validField.lang,
};
} else {
- output.params.field = field.name;
+ output.params.field = validField.name;
}
};
}
@@ -69,28 +108,15 @@ export class FieldParamType extends BaseParamType {
const field = aggConfig.getIndexPattern().fields.getByName(fieldName);
if (!field) {
- throw new SavedObjectNotFound('index-pattern-field', fieldName);
- }
-
- const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName);
- if (!validField) {
- throw new Error(
- i18n.translate(
- 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage',
- {
- defaultMessage:
- 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.',
- values: {
- fieldParameter: fieldName,
- aggType: aggConfig?.type?.title,
- indexPatternTitle: aggConfig.getIndexPattern().title,
- },
- }
- )
- );
+ return new IndexPatternField({
+ type: KBN_FIELD_TYPES.MISSING,
+ name: fieldName,
+ searchable: false,
+ aggregatable: false,
+ });
}
- return validField;
+ return field;
};
}
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index c47cd6cd9740d..d2683e248b7bf 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -388,6 +388,8 @@ export {
PainlessError,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './search';
export type {
@@ -467,6 +469,7 @@ export {
TimeHistoryContract,
QueryStateChange,
QueryStart,
+ AutoRefreshDoneFn,
} from './query';
export { AggsStart } from './search/aggs';
diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
index fa33f00a49879..36569cafd6611 100644
--- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts
+++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
@@ -9,6 +9,7 @@
import sinon from 'sinon';
import { CoreSetup } from 'src/core/public';
+import { SerializedFieldFormat } from 'src/plugins/expressions/public';
import { IFieldType, FieldSpec } from '../../common/index_patterns';
import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../';
import { getFieldFormatsRegistry } from '../test_utils';
@@ -51,6 +52,7 @@ export class StubIndexPattern {
_reindexFields: Function;
stubSetFieldFormat: Function;
fields?: FieldSpec[];
+ setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
constructor(
pattern: string,
@@ -74,6 +76,10 @@ export class StubIndexPattern {
this.metaFields = ['_id', '_type', '_source'];
this.fieldFormatMap = {};
+ this.setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => {
+ this.fieldFormatMap[fieldName] = format;
+ };
+
this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this);
this.flattenHit = indexPatterns.flattenHitWrapper(
(this as unknown) as IndexPattern,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index ec24a9296674d..05925f097de24 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext {
// @public (undocumented)
export type AutocompleteStart = ReturnType;
+// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export type AutoRefreshDoneFn = () => void;
+
// Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1781,6 +1786,8 @@ export enum KBN_FIELD_TYPES {
// (undocumented)
IP_RANGE = "ip_range",
// (undocumented)
+ MISSING = "missing",
+ // (undocumented)
MURMUR3 = "murmur3",
// (undocumented)
NESTED = "nested",
@@ -2647,6 +2654,18 @@ export const UI_SETTINGS: {
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
};
+// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+
+// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export interface WaitUntilNextSessionCompletesOptions {
+ waitForIdle?: number;
+}
+
// Warnings were encountered during analysis:
//
@@ -2694,21 +2713,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts
index 83e897824d86c..3dfd4e0fe514f 100644
--- a/src/plugins/data/public/query/timefilter/index.ts
+++ b/src/plugins/data/public/query/timefilter/index.ts
@@ -9,7 +9,7 @@
export { TimefilterService, TimefilterSetup } from './timefilter_service';
export * from './types';
-export { Timefilter, TimefilterContract } from './timefilter';
+export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter';
export { TimeHistory, TimeHistoryContract } from './time_history';
export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter';
export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter';
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
new file mode 100644
index 0000000000000..3c8b316c3b878
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop';
+
+jest.useFakeTimers();
+
+test('triggers refresh with interval', () => {
+ const { loop$, start, stop } = createAutoRefreshLoop();
+
+ const fn = jest.fn((done) => done());
+ loop$.subscribe(fn);
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).not.toBeCalled();
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ stop();
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done!: AutoRefreshDoneFn;
+ const fn = jest.fn((_done) => {
+ done = _done;
+ });
+ loop$.subscribe(fn);
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(done).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ done();
+
+ jest.advanceTimersByTime(500);
+ expect(fn).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() from multiple subscribers to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ let done2!: AutoRefreshDoneFn;
+ const fn2 = jest.fn((_done) => {
+ done2 = _done;
+ });
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done2();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('unsubscribe() resets the state', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ const sub2 = loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ sub2.unsubscribe();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('calling done() twice is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+});
+
+test('calling older done() is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ // @ts-ignore
+ if (done1) return;
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
new file mode 100644
index 0000000000000..1e213b36e1d8b
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { defer, Subject } from 'rxjs';
+import { finalize, map } from 'rxjs/operators';
+import { once } from 'lodash';
+
+export type AutoRefreshDoneFn = () => void;
+
+/**
+ * Creates a loop for timepicker's auto refresh
+ * It has a "confirmation" mechanism:
+ * When auto refresh loop emits, it won't continue automatically,
+ * until each subscriber calls received `done` function.
+ *
+ * @internal
+ */
+export const createAutoRefreshLoop = () => {
+ let subscribersCount = 0;
+ const tick = new Subject();
+
+ let _timeoutHandle: number;
+ let _timeout: number = 0;
+
+ function start() {
+ stop();
+ if (_timeout === 0) return;
+ const timeoutHandle = window.setTimeout(() => {
+ let pendingDoneCount = subscribersCount;
+ const done = () => {
+ if (timeoutHandle !== _timeoutHandle) return;
+
+ pendingDoneCount--;
+ if (pendingDoneCount === 0) {
+ start();
+ }
+ };
+ tick.next(done);
+ }, _timeout);
+
+ _timeoutHandle = timeoutHandle;
+ }
+
+ function stop() {
+ window.clearTimeout(_timeoutHandle);
+ _timeoutHandle = -1;
+ }
+
+ return {
+ stop: () => {
+ _timeout = 0;
+ stop();
+ },
+ start: (timeout: number) => {
+ _timeout = timeout;
+ if (subscribersCount > 0) {
+ start();
+ }
+ },
+ loop$: defer(() => {
+ subscribersCount++;
+ start(); // restart the loop on a new subscriber
+ return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once
+ }).pipe(
+ finalize(() => {
+ subscribersCount--;
+ if (subscribersCount === 0) {
+ stop();
+ } else {
+ start(); // restart the loop to potentially unblock the interval
+ }
+ })
+ ),
+ };
+};
diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts
index 8e1e76ed19e6d..92ee6b0c30428 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.test.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts
@@ -10,7 +10,7 @@ jest.useFakeTimers();
import sinon from 'sinon';
import moment from 'moment';
-import { Timefilter } from './timefilter';
+import { AutoRefreshDoneFn, Timefilter } from './timefilter';
import { Subscription } from 'rxjs';
import { TimeRange, RefreshInterval } from '../../../common';
import { createNowProviderMock } from '../../now_provider/mocks';
@@ -121,7 +121,7 @@ describe('setRefreshInterval', () => {
beforeEach(() => {
update = sinon.spy();
fetch = sinon.spy();
- autoRefreshFetch = sinon.spy();
+ autoRefreshFetch = sinon.spy((done) => done());
timefilter.setRefreshInterval({
pause: false,
value: 0,
@@ -344,3 +344,44 @@ describe('calculateBounds', () => {
expect(() => timefilter.calculateBounds(timeRange)).toThrowError();
});
});
+
+describe('getAutoRefreshFetch$', () => {
+ test('next auto refresh loop starts after "done" called', () => {
+ const autoRefreshFetch = jest.fn();
+ let doneCb: AutoRefreshDoneFn | undefined;
+ timefilter.getAutoRefreshFetch$().subscribe((done) => {
+ autoRefreshFetch();
+ doneCb = done;
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ if (doneCb) doneCb();
+
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+
+ test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => {
+ const autoRefreshFetch = jest.fn();
+ const fetch$ = timefilter.getAutoRefreshFetch$();
+ const sub1 = fetch$.subscribe((done) => {
+ autoRefreshFetch();
+ // this done will be never called, but loop will be reset by another subscription
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ fetch$.subscribe(autoRefreshFetch);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+ sub1.unsubscribe();
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+});
diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts
index 436b18f70a2f8..9894010601d2b 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.ts
@@ -22,6 +22,9 @@ import {
TimeRange,
} from '../../../common';
import { TimeHistoryContract } from './time_history';
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop';
+
+export { AutoRefreshDoneFn };
// TODO: remove!
@@ -32,8 +35,6 @@ export class Timefilter {
private timeUpdate$ = new Subject();
// Fired when a user changes the the autorefresh settings
private refreshIntervalUpdate$ = new Subject();
- // Used when an auto refresh is triggered
- private autoRefreshFetch$ = new Subject();
private fetch$ = new Subject();
private _time: TimeRange;
@@ -45,11 +46,12 @@ export class Timefilter {
private _isTimeRangeSelectorEnabled: boolean = false;
private _isAutoRefreshSelectorEnabled: boolean = false;
- private _autoRefreshIntervalId: number = 0;
-
private readonly timeDefaults: TimeRange;
private readonly refreshIntervalDefaults: RefreshInterval;
+ // Used when an auto refresh is triggered
+ private readonly autoRefreshLoop = createAutoRefreshLoop();
+
constructor(
config: TimefilterConfig,
timeHistory: TimeHistoryContract,
@@ -86,9 +88,13 @@ export class Timefilter {
return this.refreshIntervalUpdate$.asObservable();
};
- public getAutoRefreshFetch$ = () => {
- return this.autoRefreshFetch$.asObservable();
- };
+ /**
+ * Get an observable that emits when it is time to refetch data due to refresh interval
+ * Each subscription to this observable resets internal interval
+ * Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop
+ * Apps should use this callback to start next auto refresh loop when view finished updating
+ */
+ public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$;
public getFetch$ = () => {
return this.fetch$.asObservable();
@@ -166,13 +172,9 @@ export class Timefilter {
}
}
- // Clear the previous auto refresh interval and start a new one (if not paused)
- clearInterval(this._autoRefreshIntervalId);
- if (!newRefreshInterval.pause) {
- this._autoRefreshIntervalId = window.setInterval(
- () => this.autoRefreshFetch$.next(),
- newRefreshInterval.value
- );
+ this.autoRefreshLoop.stop();
+ if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) {
+ this.autoRefreshLoop.start(newRefreshInterval.value);
}
};
diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
index 0f2b01f618186..c22f62f45a709 100644
--- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
@@ -20,7 +20,7 @@ const createSetupContractMock = () => {
getEnabledUpdated$: jest.fn(),
getTimeUpdate$: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
- getAutoRefreshFetch$: jest.fn(() => new Observable()),
+ getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()),
getFetch$: jest.fn(),
getTime: jest.fn(),
setTime: jest.fn(),
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index fded4c46992c0..92a5c36202e6f 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -45,6 +45,8 @@ export {
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './session';
export { getEsPreference } from './es_search';
diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts
index 15410400a33e6..ce578378a2fe8 100644
--- a/src/plugins/data/public/search/session/index.ts
+++ b/src/plugins/data/public/search/session/index.ts
@@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state';
export { SessionsClient, ISessionsClient } from './sessions_client';
export { noSearchSessionStorageCapabilityMessage } from './i18n';
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+export {
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
+} from './session_helpers';
diff --git a/src/plugins/data/public/search/session/session_helpers.test.ts b/src/plugins/data/public/search/session/session_helpers.test.ts
new file mode 100644
index 0000000000000..5b64e7b554d18
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.test.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { waitUntilNextSessionCompletes$ } from './session_helpers';
+import { ISessionService, SessionService } from './session_service';
+import { BehaviorSubject } from 'rxjs';
+import { SearchSessionState } from './search_session_state';
+import { NowProviderInternalContract } from '../../now_provider';
+import { coreMock } from '../../../../../core/public/mocks';
+import { createNowProviderMock } from '../../now_provider/mocks';
+import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+import { getSessionsClientMock } from './mocks';
+
+let sessionService: ISessionService;
+let state$: BehaviorSubject;
+let nowProvider: jest.Mocked;
+let currentAppId$: BehaviorSubject;
+
+beforeEach(() => {
+ const initializerContext = coreMock.createPluginInitializerContext();
+ const startService = coreMock.createSetup().getStartServices;
+ nowProvider = createNowProviderMock();
+ currentAppId$ = new BehaviorSubject('app');
+ sessionService = new SessionService(
+ initializerContext,
+ () =>
+ startService().then(([coreStart, ...rest]) => [
+ {
+ ...coreStart,
+ application: {
+ ...coreStart.application,
+ currentAppId$,
+ capabilities: {
+ ...coreStart.application.capabilities,
+ management: {
+ kibana: {
+ [SEARCH_SESSIONS_MANAGEMENT_ID]: true,
+ },
+ },
+ },
+ },
+ },
+ ...rest,
+ ]),
+ getSessionsClientMock(),
+ nowProvider,
+ { freezeState: false } // needed to use mocks inside state container
+ );
+ state$ = new BehaviorSubject(SearchSessionState.None);
+ sessionService.state$.subscribe(state$);
+});
+
+describe('waitUntilNextSessionCompletes$', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+ test('emits when next session starts', () => {
+ sessionService.start();
+ let untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ const next = jest.fn();
+ const complete = jest.fn();
+ waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete });
+ expect(next).not.toBeCalled();
+
+ sessionService.start();
+ expect(next).not.toBeCalled();
+
+ untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(500);
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(1000);
+ expect(next).toBeCalledTimes(1);
+ expect(complete).toBeCalled();
+ });
+});
diff --git a/src/plugins/data/public/search/session/session_helpers.ts b/src/plugins/data/public/search/session/session_helpers.ts
new file mode 100644
index 0000000000000..1f0a2da7e93f4
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounceTime, first, skipUntil } from 'rxjs/operators';
+import { ISessionService } from './session_service';
+import { SearchSessionState } from './search_session_state';
+
+/**
+ * Options for {@link waitUntilNextSessionCompletes$}
+ */
+export interface WaitUntilNextSessionCompletesOptions {
+ /**
+ * For how long to wait between session state transitions before considering that session completed
+ */
+ waitForIdle?: number;
+}
+
+/**
+ * Creates an observable that emits when next search session completes.
+ * This utility is helpful to use in the application to delay some tasks until next session completes.
+ *
+ * @param sessionService - {@link ISessionService}
+ * @param opts - {@link WaitUntilNextSessionCompletesOptions}
+ */
+export function waitUntilNextSessionCompletes$(
+ sessionService: ISessionService,
+ { waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 }
+) {
+ return sessionService.state$.pipe(
+ // wait until new session starts
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))),
+ // wait until new session starts loading
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))),
+ // debounce to ignore quick switches from loading <-> completed.
+ // that could happen between sequential search requests inside a single session
+ debounceTime(waitForIdle),
+ // then wait until it finishes
+ first(
+ (state) =>
+ state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted
+ )
+ );
+}
diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss
index 466cc8c3de0b7..4e12f11668734 100644
--- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss
+++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss
@@ -17,6 +17,16 @@
@include kbnThemeStyle('v8') {
background-color: $euiFormBackgroundColor;
+ border-radius: $euiFormControlBorderRadius;
+
+ &.kbnQueryBar__textareaWrap--hasPrepend {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ &.kbnQueryBar__textareaWrap--hasAppend {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
}
@@ -35,8 +45,16 @@
}
@include kbnThemeStyle('v8') {
- border-radius: 0;
padding-bottom: $euiSizeS + 1px;
+
+ &.kbnQueryBar__textarea--hasPrepend {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ &.kbnQueryBar__textarea--hasAppend {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
&:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) {
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index 900a4ab7d7eb7..0f660f87266fd 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -682,7 +682,14 @@ export default class QueryStringInputUI extends Component {
);
const inputClassName = classNames(
'kbnQueryBar__textarea',
- this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null
+ this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null,
+ this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null,
+ !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null
+ );
+ const inputWrapClassName = classNames(
+ 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap',
+ this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null,
+ !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null
);
return (
@@ -711,7 +718,7 @@ export default class QueryStringInputUI extends Component {
>
{
+ autoRefreshDoneCb = done;
+ }),
+ filter(() => $scope.fetchStatus !== fetchStatuses.LOADING)
+ ),
data.query.queryString.getUpdates$(),
searchSessionManager.newSearchSessionIdFromURL$
).pipe(debounceTime(100));
@@ -508,7 +515,16 @@ function discoverController($route, $scope) {
$scope,
fetch$,
{
- next: $scope.fetch,
+ next: async () => {
+ try {
+ await $scope.fetch();
+ } finally {
+ // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that
+ // the last fetch is completed so it starts the next auto refresh loop if needed
+ autoRefreshDoneCb?.();
+ autoRefreshDoneCb = undefined;
+ }
+ },
},
(error) => addFatalError(core.fatalErrors, error)
)
diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
index f3c8990388024..f976b961d8520 100644
--- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
@@ -543,6 +543,7 @@ exports[`discover sidebar field details footer renders properly 1`] = `
"_source",
],
"popularizeField": [Function],
+ "setFieldFormat": [Function],
"stubSetFieldFormat": [Function],
"timeFieldName": "time",
"title": "logstash-*",
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index e8418970d83f7..a0cd213b7bf24 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -9,7 +9,7 @@
import { cloneDeep, isEqual } from 'lodash';
import * as Rx from 'rxjs';
import { merge } from 'rxjs';
-import { debounceTime, distinctUntilChanged, map, mapTo, skip } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators';
import { RenderCompleteDispatcher } from '../../../../kibana_utils/public';
import { Adapters } from '../types';
import { IContainer } from '../containers';
@@ -111,10 +111,9 @@ export abstract class Embeddable<
* In case corresponding state change triggered `reload` this stream is guarantied to emit later,
* which allows to skip any state handling in case `reload` already handled it.
*/
- public getUpdated$(): Readonly> {
+ public getUpdated$(): Readonly> {
return merge(this.getInput$().pipe(skip(1)), this.getOutput$().pipe(skip(1))).pipe(
- debounceTime(0),
- mapTo(undefined)
+ debounceTime(0)
);
}
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index b9719542adc81..3f0907acabdfa 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -282,7 +282,7 @@ export abstract class Embeddable>;
+ getUpdated$(): Readonly>;
// (undocumented)
readonly id: string;
// (undocumented)
diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts
index 65925b5a2e4c2..4165b8906a20e 100644
--- a/src/plugins/expressions/public/loader.ts
+++ b/src/plugins/expressions/public/loader.ts
@@ -118,12 +118,15 @@ export class ExpressionLoader {
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
}
- update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
+ async update(
+ expression?: string | ExpressionAstExpression,
+ params?: IExpressionLoaderParams
+ ): Promise {
this.setParams(params);
this.loadingSubject.next(true);
if (expression) {
- this.loadData(expression, this.params);
+ await this.loadData(expression, this.params);
} else if (this.data) {
this.render(this.data);
}
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
index e0ca654c956c6..13830f9233b5e 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
@@ -143,7 +143,7 @@ const FieldEditorFlyoutContentComponent = ({
const [isValidating, setIsValidating] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
- const [confirmContent, setConfirmContent] = useState();
+ const [confirmContent, setConfirmContent] = useState('');
const { submit, isValid: isFormValid, isSubmitted } = formState;
const { fields } = indexPattern;
diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json
index d664d936f6667..ee07dfe589e4a 100644
--- a/src/plugins/kibana_usage_collection/tsconfig.json
+++ b/src/plugins/kibana_usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"common/*",
diff --git a/src/plugins/kibana_utils/common/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts
index 7a9495cc8f413..7f3efc6d9571f 100644
--- a/src/plugins/kibana_utils/common/errors/errors.ts
+++ b/src/plugins/kibana_utils/common/errors/errors.ts
@@ -32,7 +32,7 @@ export class DuplicateField extends KbnError {
export class SavedObjectNotFound extends KbnError {
public savedObjectType: string;
public savedObjectId?: string;
- constructor(type: string, id?: string, link?: string) {
+ constructor(type: string, id?: string, link?: string, customMessage?: string) {
const idMsg = id ? ` (id: ${id})` : '';
let message = `Could not locate that ${type}${idMsg}`;
@@ -40,13 +40,31 @@ export class SavedObjectNotFound extends KbnError {
message += `, [click here to re-create it](${link})`;
}
- super(message);
+ super(customMessage || message);
this.savedObjectType = type;
this.savedObjectId = id;
}
}
+/**
+ * A saved field doesn't exist anymore
+ */
+export class SavedFieldNotFound extends KbnError {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
+/**
+ * A saved field type isn't compatible with aggregation
+ */
+export class SavedFieldTypeInvalidForAgg extends KbnError {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
/**
* This error is for scenarios where a saved object is detected that has invalid JSON properties.
* There was a scenario where we were importing objects with double-encoded JSON, and the system
diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts
index 84b6486f35b24..cc4ff102742d7 100644
--- a/src/plugins/telemetry/common/telemetry_config/index.ts
+++ b/src/plugins/telemetry/common/telemetry_config/index.ts
@@ -9,7 +9,5 @@
export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
-export {
- getTelemetryFailureDetails,
- TelemetryFailureDetails,
-} from './get_telemetry_failure_details';
+export { getTelemetryFailureDetails } from './get_telemetry_failure_details';
+export type { TelemetryFailureDetails } from './get_telemetry_failure_details';
diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts
index 6cca9bdf881dd..47ba7828eaec2 100644
--- a/src/plugins/telemetry/public/index.ts
+++ b/src/plugins/telemetry/public/index.ts
@@ -8,7 +8,7 @@
import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
-export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
+export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryPlugin(initializerContext);
diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts
index debdf7515cd58..1c335426ffd03 100644
--- a/src/plugins/telemetry/server/index.ts
+++ b/src/plugins/telemetry/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config';
export { FetcherTask } from './fetcher';
export { handleOldSettings } from './handle_old_settings';
-export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
+export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
export const config: PluginConfigDescriptor = {
schema: configSchema,
@@ -34,9 +34,12 @@ export { constants };
export {
getClusterUuids,
getLocalStats,
- TelemetryLocalStats,
DATA_TELEMETRY_ID,
+ buildDataTelemetryPayload,
+} from './telemetry_collection';
+
+export type {
+ TelemetryLocalStats,
DataTelemetryIndex,
DataTelemetryPayload,
- buildDataTelemetryPayload,
} from './telemetry_collection';
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
index def1131dfb1a3..c93b7e872924b 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
@@ -7,10 +7,5 @@
*/
export { DATA_TELEMETRY_ID } from './constants';
-
-export {
- getDataTelemetry,
- buildDataTelemetryPayload,
- DataTelemetryPayload,
- DataTelemetryIndex,
-} from './get_data_telemetry';
+export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry';
diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts
index 55f9c7f0e624c..151e89a11a192 100644
--- a/src/plugins/telemetry/server/telemetry_collection/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/index.ts
@@ -6,12 +6,9 @@
* Side Public License, v 1.
*/
-export {
- DATA_TELEMETRY_ID,
- DataTelemetryIndex,
- DataTelemetryPayload,
- buildDataTelemetryPayload,
-} from './get_data_telemetry';
-export { getLocalStats, TelemetryLocalStats } from './get_local_stats';
+export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry';
+export { getLocalStats } from './get_local_stats';
+export type { TelemetryLocalStats } from './get_local_stats';
export { getClusterUuids } from './get_cluster_stats';
export { registerCollection } from './register_collection';
diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts
index 4e3f046f7611f..594b53259a65f 100644
--- a/src/plugins/telemetry/server/telemetry_repository/index.ts
+++ b/src/plugins/telemetry/server/telemetry_repository/index.ts
@@ -8,7 +8,7 @@
export { getTelemetrySavedObject } from './get_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
-export {
+export type {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
} from '../../common/telemetry_config/types';
diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json
index bdced01d9eb6f..6629e479906c9 100644
--- a/src/plugins/telemetry/tsconfig.json
+++ b/src/plugins/telemetry/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/**/*",
diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts
index 77077b73cf8ad..c0cd124a132c0 100644
--- a/src/plugins/telemetry_collection_manager/server/index.ts
+++ b/src/plugins/telemetry_collection_manager/server/index.ts
@@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryCollectionManagerPlugin(initializerContext);
}
-export {
+export type {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
StatsCollectionConfig,
diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json
index 1bba81769f0dd..1329979860603 100644
--- a/src/plugins/telemetry_collection_manager/tsconfig.json
+++ b/src/plugins/telemetry_collection_manager/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"server/**/*",
diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts
index 28b04418f512d..db6ea17556ed3 100644
--- a/src/plugins/telemetry_management_section/public/index.ts
+++ b/src/plugins/telemetry_management_section/public/index.ts
@@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin';
export { OptInExampleFlyout } from './components';
-export { TelemetryManagementSectionPluginSetup } from './plugin';
+export type { TelemetryManagementSectionPluginSetup } from './plugin';
export function plugin() {
return new TelemetryManagementSectionPlugin();
}
diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json
index 48e40814b8570..2daee868ac200 100644
--- a/src/plugins/telemetry_management_section/tsconfig.json
+++ b/src/plugins/telemetry_management_section/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts
index b9e0e0a8985b1..9b009b1d9e264 100644
--- a/src/plugins/usage_collection/public/index.ts
+++ b/src/plugins/usage_collection/public/index.ts
@@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public';
import { UsageCollectionPlugin } from './plugin';
export { METRIC_TYPE } from '@kbn/analytics';
-export { UsageCollectionSetup, UsageCollectionStart } from './plugin';
+export type { UsageCollectionSetup, UsageCollectionStart } from './plugin';
export { TrackApplicationView } from './components';
export function plugin(initializerContext: PluginInitializerContext) {
diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts
index 5f48f9fb93813..d5e0d95659e58 100644
--- a/src/plugins/usage_collection/server/collector/index.ts
+++ b/src/plugins/usage_collection/server/collector/index.ts
@@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
-export { CollectorSet, CollectorSetPublic } from './collector_set';
-export {
- Collector,
+export { CollectorSet } from './collector_set';
+export type { CollectorSetPublic } from './collector_set';
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
AllowedSchemaNumberTypes,
SchemaField,
@@ -16,4 +17,5 @@ export {
CollectorOptions,
CollectorFetchContext,
} from './collector';
-export { UsageCollector, UsageCollectorOptions } from './usage_collector';
+export { UsageCollector } from './usage_collector';
+export type { UsageCollectorOptions } from './usage_collector';
diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts
index dfc9d19b69646..dd9e6644a827d 100644
--- a/src/plugins/usage_collection/server/index.ts
+++ b/src/plugins/usage_collection/server/index.ts
@@ -9,17 +9,16 @@
import { PluginInitializerContext } from 'src/core/server';
import { UsageCollectionPlugin } from './plugin';
-export {
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
MakeSchemaFrom,
SchemaField,
CollectorOptions,
UsageCollectorOptions,
- Collector,
CollectorFetchContext,
} from './collector';
-
-export { UsageCollectionSetup } from './plugin';
+export type { UsageCollectionSetup } from './plugin';
export { config } from './config';
export const plugin = (initializerContext: PluginInitializerContext) =>
new UsageCollectionPlugin(initializerContext);
diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts
index 1a60d84e7948c..7e3f4273bbea8 100644
--- a/src/plugins/usage_collection/server/usage_collection.mock.ts
+++ b/src/plugins/usage_collection/server/usage_collection.mock.ts
@@ -16,7 +16,8 @@ import {
import { CollectorOptions, Collector, UsageCollector } from './collector';
import { UsageCollectionSetup, CollectorFetchContext } from './index';
-export { CollectorOptions, Collector };
+export type { CollectorOptions };
+export { Collector };
const logger = loggingSystemMock.createLogger();
diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json
index 96b2c4d37e17c..68a0853994e80 100644
--- a/src/plugins/usage_collection/tsconfig.json
+++ b/src/plugins/usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss
index c412b9d915e55..56c6a0f0f63f6 100644
--- a/src/plugins/vis_default_editor/public/_default.scss
+++ b/src/plugins/vis_default_editor/public/_default.scss
@@ -1,6 +1,4 @@
.visEditor--default {
- // height: 1px is in place to make editor children take their height in the parent
- height: 1px;
flex: 1 1 auto;
display: flex;
}
@@ -80,6 +78,7 @@
.visEditor__collapsibleSidebar {
width: 100% !important; // force the editor to take 100% width
+ flex-grow: 0;
}
.visEditor__collapsibleSidebar-isClosed {
@@ -91,8 +90,10 @@
}
.visEditor__visualization__wrapper {
- // force the visualization to take 100% width and height.
+ // force the visualization to take 100% width.
width: 100% !important;
- height: 100% !important;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
}
diff --git a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
index 94f767510c4bd..277804567c2b7 100644
--- a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
@@ -11,7 +11,7 @@ import { act } from 'react-dom/test-utils';
import { mount, shallow, ReactWrapper } from 'enzyme';
import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui';
-import { IAggConfig, IndexPatternField } from 'src/plugins/data/public';
+import { IAggConfig, IndexPatternField, AggParam } from 'src/plugins/data/public';
import { ComboBoxGroupedOptions } from '../../utils';
import { FieldParamEditor, FieldParamEditorProps } from './field';
import { EditorVisState } from '../sidebar/state/reducers';
@@ -42,7 +42,7 @@ describe('FieldParamEditor component', () => {
setTouched = jest.fn();
onChange = jest.fn();
- field = { displayName: 'bytes' } as IndexPatternField;
+ field = { displayName: 'bytes', type: 'bytes' } as IndexPatternField;
option = { label: 'bytes', target: field };
indexedFields = [
{
@@ -52,7 +52,16 @@ describe('FieldParamEditor component', () => {
];
defaultProps = {
- agg: {} as IAggConfig,
+ agg: {
+ type: {
+ params: [
+ ({
+ name: 'field',
+ filterFieldTypes: ['bytes'],
+ } as unknown) as AggParam,
+ ],
+ },
+ } as IAggConfig,
aggParam: {
name: 'field',
type: 'field',
diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx
index 95843dc6ae3a8..f8db2d89888a2 100644
--- a/src/plugins/vis_default_editor/public/components/controls/field.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx
@@ -13,7 +13,13 @@ import useMount from 'react-use/lib/useMount';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { AggParam, IAggConfig, IFieldParamType, IndexPatternField } from 'src/plugins/data/public';
+import {
+ AggParam,
+ IAggConfig,
+ IFieldParamType,
+ IndexPatternField,
+ KBN_FIELD_TYPES,
+} from '../../../../../plugins/data/public';
import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils';
import { AggParamEditorProps } from '../agg_param_props';
import { ComboBoxGroupedOptions } from '../../utils';
@@ -55,6 +61,7 @@ function FieldParamEditor({
}
};
const errors = customError ? [customError] : [];
+ let showErrorMessageImmediately = false;
if (!indexedFields.length) {
errors.push(
@@ -69,9 +76,38 @@ function FieldParamEditor({
);
}
+ if (value && value.type === KBN_FIELD_TYPES.MISSING) {
+ errors.push(
+ i18n.translate('visDefaultEditor.controls.field.fieldIsNotExists', {
+ defaultMessage:
+ 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.',
+ values: {
+ fieldParameter: value.name,
+ },
+ })
+ );
+ showErrorMessageImmediately = true;
+ } else if (
+ value &&
+ !getFieldTypes(agg).find((type: string) => type === value.type || type === '*')
+ ) {
+ errors.push(
+ i18n.translate('visDefaultEditor.controls.field.invalidFieldForAggregation', {
+ defaultMessage:
+ 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with this aggregation. Please select a new field.',
+ values: {
+ fieldParameter: value?.name,
+ indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title,
+ },
+ })
+ );
+ showErrorMessageImmediately = true;
+ }
+
const isValid = !!value && !errors.length && !isDirty;
// we show an error message right away if there is no compatible fields
- const showErrorMessage = (showValidation || !indexedFields.length) && !isValid;
+ const showErrorMessage =
+ (showValidation || !indexedFields.length || showErrorMessageImmediately) && !isValid;
useValidation(setValidity, isValid);
useMount(() => {
@@ -122,10 +158,14 @@ function FieldParamEditor({
}
function getFieldTypesString(agg: IAggConfig) {
+ return formatListAsProse(getFieldTypes(agg), { inclusive: false });
+}
+
+function getFieldTypes(agg: IAggConfig) {
const param =
get(agg, 'type.params', []).find((p: AggParam) => p.name === 'field') ||
({} as IFieldParamType);
- return formatListAsProse(parseCommaSeparatedList(param.filterFieldTypes), { inclusive: false });
+ return parseCommaSeparatedList(param.filterFieldTypes || []);
}
export { FieldParamEditor };
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 429dabeeef042..3bb52eb15758a 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -149,8 +149,9 @@ export class VisualizeEmbeddable
}
this.subscriptions.push(
- this.getUpdated$().subscribe(() => {
+ this.getUpdated$().subscribe((value) => {
const isDirty = this.handleChanges();
+
if (isDirty && this.handler) {
this.updateHandler();
}
@@ -367,8 +368,8 @@ export class VisualizeEmbeddable
}
}
- public reload = () => {
- this.handleVisUpdate();
+ public reload = async () => {
+ await this.handleVisUpdate();
};
private async updateHandler() {
@@ -395,13 +396,13 @@ export class VisualizeEmbeddable
});
if (this.handler && !abortController.signal.aborted) {
- this.handler.update(this.expression, expressionParams);
+ await this.handler.update(this.expression, expressionParams);
}
}
private handleVisUpdate = async () => {
this.handleChanges();
- this.updateHandler();
+ await this.updateHandler();
};
private uiStateChangeHandler = () => {
diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
index 256e634ac6c40..f6ef1caf9c9e0 100644
--- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
@@ -183,8 +183,12 @@ const TopNav = ({
useEffect(() => {
const autoRefreshFetchSub = services.data.query.timefilter.timefilter
.getAutoRefreshFetch$()
- .subscribe(() => {
- visInstance.embeddableHandler.reload();
+ .subscribe(async (done) => {
+ try {
+ await visInstance.embeddableHandler.reload();
+ } finally {
+ done();
+ }
});
return () => {
autoRefreshFetchSub.unsubscribe();
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
index cc0f3ce2afae5..9eda709e58c3e 100644
--- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
@@ -18,8 +18,17 @@ import { SavedObject } from 'src/plugins/saved_objects/public';
import { cloneDeep } from 'lodash';
import { ExpressionValueError } from 'src/plugins/expressions/public';
import { createSavedSearchesLoader } from '../../../../discover/public';
+import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common';
import { VisualizeServices } from '../types';
+function isErrorRelatedToRuntimeFields(error: ExpressionValueError['error']) {
+ const originalError = error.original || error;
+ return (
+ originalError instanceof SavedFieldNotFound ||
+ originalError instanceof SavedFieldTypeInvalidForAgg
+ );
+}
+
const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
@@ -37,7 +46,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
})) as VisualizeEmbeddableContract;
embeddableHandler.getOutput$().subscribe((output) => {
- if (output.error) {
+ if (output.error && !isErrorRelatedToRuntimeFields(output.error)) {
data.search.showError(
((output.error as unknown) as ExpressionValueError['error']).original || output.error
);
diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
index 64d61996495d7..965951bfbd88d 100644
--- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
+++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
@@ -11,13 +11,12 @@ import { EventEmitter } from 'events';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
-import { redirectWhenMissing } from '../../../../../kibana_utils/public';
-
import { getVisualizationInstance } from '../get_visualization_instance';
import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types';
import { VisualizeConstants } from '../../visualize_constants';
import { getVisEditorsRegistry } from '../../../services';
+import { redirectToSavedObjectPage } from '../utils';
/**
* This effect is responsible for instantiating a saved vis or creating a new one
@@ -43,9 +42,7 @@ export const useSavedVisInstance = (
chrome,
history,
dashboard,
- setActiveUrl,
toastNotifications,
- http: { basePath },
stateTransferService,
application: { navigateToApp },
} = services;
@@ -131,27 +128,8 @@ export const useSavedVisInstance = (
visEditorController,
});
} catch (error) {
- const managementRedirectTarget = {
- app: 'management',
- path: `kibana/objects/savedVisualizations/${visualizationIdFromUrl}`,
- };
-
try {
- redirectWhenMissing({
- history,
- navigateToApp,
- toastNotifications,
- basePath,
- mapping: {
- visualization: VisualizeConstants.LANDING_PAGE_PATH,
- search: managementRedirectTarget,
- 'index-pattern': managementRedirectTarget,
- 'index-pattern-field': managementRedirectTarget,
- },
- onBeforeRedirect() {
- setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
- },
- })(error);
+ redirectToSavedObjectPage(services, error, visualizationIdFromUrl);
} catch (e) {
toastNotifications.addWarning({
title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', {
diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts
index 0e529507f97e3..c906ff5304c90 100644
--- a/src/plugins/visualize/public/application/utils/utils.ts
+++ b/src/plugins/visualize/public/application/utils/utils.ts
@@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
import { ChromeStart, DocLinksStart } from 'kibana/public';
import { Filter } from '../../../../data/public';
+import { redirectWhenMissing } from '../../../../kibana_utils/public';
+import { VisualizeConstants } from '../visualize_constants';
import { VisualizeServices, VisualizeEditorVisInstance } from '../types';
export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => {
@@ -58,3 +60,36 @@ export const visStateToEditorState = (
linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId,
};
};
+
+export const redirectToSavedObjectPage = (
+ services: VisualizeServices,
+ error: any,
+ savedVisualizationsId?: string
+) => {
+ const {
+ history,
+ setActiveUrl,
+ toastNotifications,
+ http: { basePath },
+ application: { navigateToApp },
+ } = services;
+ const managementRedirectTarget = {
+ app: 'management',
+ path: `kibana/objects/savedVisualizations/${savedVisualizationsId}`,
+ };
+ redirectWhenMissing({
+ history,
+ navigateToApp,
+ toastNotifications,
+ basePath,
+ mapping: {
+ visualization: VisualizeConstants.LANDING_PAGE_PATH,
+ search: managementRedirectTarget,
+ 'index-pattern': managementRedirectTarget,
+ 'index-pattern-field': managementRedirectTarget,
+ },
+ onBeforeRedirect() {
+ setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
+ },
+ })(error);
+};
diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts
index db9c996147c94..f6131679874db 100644
--- a/x-pack/examples/alerting_example/server/plugin.ts
+++ b/x-pack/examples/alerting_example/server/plugin.ts
@@ -33,7 +33,7 @@ export class AlertingExamplePlugin implements Plugin {
rejectUnauthorized: true,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration('60s'),
});
const localActionTypeRegistryParams = {
diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts
index 012cd63be2702..76f6a62ce6597 100644
--- a/x-pack/plugins/actions/server/actions_config.mock.ts
+++ b/x-pack/plugins/actions/server/actions_config.mock.ts
@@ -17,6 +17,10 @@ const createActionsConfigMock = () => {
ensureActionTypeEnabled: jest.fn().mockReturnValue({}),
isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true),
getProxySettings: jest.fn().mockReturnValue(undefined),
+ getResponseSettings: jest.fn().mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ }),
};
return mocked;
};
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index 36899f7661ba4..c81f1f4a4bf2e 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -5,12 +5,14 @@
* 2.0.
*/
+import { ByteSizeValue } from '@kbn/config-schema';
import { ActionsConfig } from './config';
import {
getActionsConfigurationUtilities,
AllowedHosts,
EnabledActionTypes,
} from './actions_config';
+import moment from 'moment';
const defaultActionsConfig: ActionsConfig = {
enabled: false,
@@ -19,6 +21,8 @@ const defaultActionsConfig: ActionsConfig = {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
};
describe('ensureUriAllowed', () => {
@@ -254,6 +258,18 @@ describe('ensureActionTypeEnabled', () => {
});
});
+describe('getResponseSettingsFromConfig', () => {
+ test('returns expected parsed values for default config for responseTimeout and maxResponseContentLength', () => {
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ };
+ expect(getActionsConfigurationUtilities(config).getResponseSettings()).toEqual({
+ timeout: 60000,
+ maxContentLength: 1000000,
+ });
+ });
+});
+
describe('getProxySettings', () => {
test('returns undefined when no proxy URL set', () => {
const config: ActionsConfig = {
diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts
index b35a4a0d7b6c5..4c73cab76f9e8 100644
--- a/x-pack/plugins/actions/server/actions_config.ts
+++ b/x-pack/plugins/actions/server/actions_config.ts
@@ -13,7 +13,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config';
import { ActionTypeDisabledError } from './lib';
-import { ProxySettings } from './types';
+import { ProxySettings, ResponseSettings } from './types';
export { AllowedHosts, EnabledActionTypes } from './config';
@@ -31,6 +31,7 @@ export interface ActionsConfigurationUtilities {
ensureActionTypeEnabled: (actionType: string) => void;
isRejectUnauthorizedCertificatesEnabled: () => boolean;
getProxySettings: () => undefined | ProxySettings;
+ getResponseSettings: () => ResponseSettings;
}
function allowListErrorMessage(field: AllowListingField, value: string) {
@@ -99,6 +100,13 @@ function arrayAsSet(arr: T[] | undefined): Set | undefined {
return new Set(arr);
}
+function getResponseSettingsFromConfig(config: ActionsConfig): ResponseSettings {
+ return {
+ maxContentLength: config.maxResponseContentLength.getValueInBytes(),
+ timeout: config.responseTimeout.asMilliseconds(),
+ };
+}
+
export function getActionsConfigurationUtilities(
config: ActionsConfig
): ActionsConfigurationUtilities {
@@ -110,6 +118,7 @@ export function getActionsConfigurationUtilities(
isUriAllowed,
isActionTypeEnabled,
getProxySettings: () => getProxySettingsFromConfig(config),
+ getResponseSettings: () => getResponseSettingsFromConfig(config),
isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized,
ensureUriAllowed(uri: string) {
if (!isUriAllowed(uri)) {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
index b858d5491a6bd..4596619c50940 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
@@ -283,6 +283,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -342,6 +343,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
index a932b38ede2bb..edc9429e4fac6 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
@@ -42,6 +42,10 @@ describe('request', () => {
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
}));
+ configurationUtilities.getResponseSettings.mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ });
});
test('it fetch correctly with defaults', async () => {
@@ -58,6 +62,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -88,6 +94,8 @@ describe('request', () => {
httpAgent,
httpsAgent,
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -116,6 +124,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -224,6 +234,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -235,10 +247,15 @@ describe('request', () => {
describe('patch', () => {
beforeEach(() => {
+ jest.resetAllMocks();
axiosMock.mockImplementation(() => ({
status: 200,
headers: { 'content-type': 'application/json' },
}));
+ configurationUtilities.getResponseSettings.mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ });
});
test('it fetch correctly', async () => {
@@ -249,6 +266,8 @@ describe('patch', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
index edce369096142..af353e1d1da5a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
@@ -31,6 +31,7 @@ export const request = async ({
auth?: AxiosBasicCredentials;
}): Promise => {
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url);
+ const { maxContentLength, timeout } = configurationUtilities.getResponseSettings();
return await axios(url, {
...rest,
@@ -40,6 +41,8 @@ export const request = async ({
httpAgent,
httpsAgent,
proxy: false,
+ maxContentLength,
+ timeout,
});
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
index c31adddc5a57e..8a185d353de02 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
@@ -168,6 +168,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -230,6 +231,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
index c468453247809..d3f059eede615 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
@@ -291,6 +291,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -329,6 +330,33 @@ describe('execute()', () => {
`);
});
+ test('execute with exception maxContentLength size exceeded should log the proper error', async () => {
+ const config: ActionTypeConfigType = {
+ url: 'https://abc.def/my-webhook',
+ method: WebhookMethods.POST,
+ headers: {
+ aheader: 'a value',
+ },
+ hasAuth: true,
+ };
+ requestMock.mockReset();
+ requestMock.mockRejectedValueOnce({
+ tag: 'err',
+ isAxiosError: true,
+ message: 'maxContentLength size of 1000000 exceeded',
+ });
+ await actionType.executor({
+ actionId: 'some-id',
+ services,
+ config,
+ secrets: { user: 'abc', password: '123' },
+ params: { body: 'some data' },
+ });
+ expect(mockedLogger.error).toBeCalledWith(
+ 'error on some-id webhook event: maxContentLength size of 1000000 exceeded'
+ );
+ });
+
test('execute without username/password sends request without basic auth', async () => {
const config: ActionTypeConfigType = {
url: 'https://abc.def/my-webhook',
@@ -355,6 +383,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
index 269449686acf0..93c9bbdbab18a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
@@ -180,7 +180,6 @@ export async function executor(
return successResult(actionId, data);
} else {
const { error } = result;
-
if (error.response) {
const {
status,
@@ -211,6 +210,10 @@ export async function executor(
const message = `[${error.code}] ${error.message}`;
logger.error(`error on ${actionId} webhook event: ${message}`);
return errorResultRequestFailed(actionId, message);
+ } else if (error.isAxiosError) {
+ const message = `${error.message}`;
+ logger.error(`error on ${actionId} webhook event: ${message}`);
+ return errorResultRequestFailed(actionId, message);
}
logger.error(`error on ${actionId} webhook action: unexpected error`);
diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts
index 0d270512d1dee..2eecaa19da0c5 100644
--- a/x-pack/plugins/actions/server/config.test.ts
+++ b/x-pack/plugins/actions/server/config.test.ts
@@ -27,9 +27,13 @@ describe('config validation', () => {
"enabledActionTypes": Array [
"*",
],
+ "maxResponseContentLength": ByteSizeValue {
+ "valueInBytes": 1048576,
+ },
"preconfigured": Object {},
"proxyRejectUnauthorizedCertificates": true,
"rejectUnauthorized": true,
+ "responseTimeout": "PT1M",
}
`);
});
@@ -57,6 +61,9 @@ describe('config validation', () => {
"enabledActionTypes": Array [
"*",
],
+ "maxResponseContentLength": ByteSizeValue {
+ "valueInBytes": 1048576,
+ },
"preconfigured": Object {
"mySlack1": Object {
"actionTypeId": ".slack",
@@ -69,6 +76,7 @@ describe('config validation', () => {
},
"proxyRejectUnauthorizedCertificates": false,
"rejectUnauthorized": false,
+ "responseTimeout": "PT1M",
}
`);
});
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index 450f03308ab0b..4aa77ded315b8 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -47,6 +47,8 @@ export const configSchema = schema.object({
proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
rejectUnauthorized: schema.boolean({ defaultValue: true }),
+ maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }),
+ responseTimeout: schema.duration({ defaultValue: '60s' }),
});
export type ActionsConfig = TypeOf;
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index b8f83e91239e2..30bbedbedbe9c 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import moment from 'moment';
+import { ByteSizeValue } from '@kbn/config-schema';
import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server';
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks';
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
@@ -37,6 +39,8 @@ describe('Actions Plugin', () => {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
@@ -197,6 +201,8 @@ describe('Actions Plugin', () => {
},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index 6830f013ade5f..b7a6750a520ea 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -138,3 +138,8 @@ export interface ProxySettings {
proxyHeaders?: Record;
proxyRejectUnauthorizedCertificates: boolean;
}
+
+export interface ResponseSettings {
+ maxContentLength: number;
+ timeout: number;
+}
diff --git a/x-pack/plugins/apm/common/latency_aggregation_types.ts b/x-pack/plugins/apm/common/latency_aggregation_types.ts
index d9db58f223144..964d6f4ed1015 100644
--- a/x-pack/plugins/apm/common/latency_aggregation_types.ts
+++ b/x-pack/plugins/apm/common/latency_aggregation_types.ts
@@ -14,7 +14,7 @@ export enum LatencyAggregationType {
}
export const latencyAggregationTypeRt = t.union([
- t.literal('avg'),
- t.literal('p95'),
- t.literal('p99'),
+ t.literal(LatencyAggregationType.avg),
+ t.literal(LatencyAggregationType.p95),
+ t.literal(LatencyAggregationType.p99),
]);
diff --git a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
index 1a17f82a52141..970e39bc4f86f 100644
--- a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
+++ b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
@@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type(
? t.failure(input, context)
: t.success(epochDate);
}),
- (a) => {
- const d = new Date(a);
- return d.toISOString();
- }
+ (output) => new Date(output).toISOString()
);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
index 7ef3cbca3ad2f..b338d1e4ab03d 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
@@ -19,7 +19,7 @@ import { useLicenseContext } from '../../../context/license/use_license_context'
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { DatePicker } from '../../shared/DatePicker';
-import { LicensePrompt } from '../../shared/LicensePrompt';
+import { LicensePrompt } from '../../shared/license_prompt';
import { Controls } from './Controls';
import { Cytoscape } from './Cytoscape';
import { getCytoscapeDivStyle } from './cytoscape_options';
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx
deleted file mode 100644
index 0312b802df173..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect, useState } from 'react';
-import {
- EuiPanel,
- EuiText,
- EuiSpacer,
- EuiLink,
- EuiToolTip,
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { debounce } from 'lodash';
-import { Filter } from '../../../../../../../common/custom_link/custom_link_types';
-import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
-import { callApmApi } from '../../../../../../services/rest/createCallApmApi';
-import { replaceTemplateVariables, convertFiltersToQuery } from './helper';
-
-interface Props {
- label: string;
- url: string;
- filters: Filter[];
-}
-
-const fetchTransaction = debounce(
- async (filters: Filter[], callback: (transaction: Transaction) => void) => {
- const transaction = await callApmApi({
- signal: null,
- endpoint: 'GET /api/apm/settings/custom_links/transaction',
- params: { query: convertFiltersToQuery(filters) },
- });
- callback(transaction);
- },
- 1000
-);
-
-const getTextColor = (value?: string) => (value ? 'default' : 'subdued');
-
-export function LinkPreview({ label, url, filters }: Props) {
- const [transaction, setTransaction] = useState();
-
- useEffect(() => {
- /*
- React throwns "Can't perform a React state update on an unmounted component"
- It happens when the Custom Link flyout is closed before the return of the api request.
- To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction.
- */
- let isUnmounted = false;
- fetchTransaction(filters, (_transaction: Transaction) => {
- if (!isUnmounted) {
- setTransaction(_transaction);
- }
- });
- return () => {
- isUnmounted = true;
- };
- }, [filters]);
-
- const { formattedUrl, error } = replaceTemplateVariables(url, transaction);
-
- return (
-
-
- {label
- ? label
- : i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.default.label',
- { defaultMessage: 'Elastic.co' }
- )}
-
-
-
- {url ? (
-
- {formattedUrl}
-
- ) : (
- i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.default.url',
- { defaultMessage: 'https://www.elastic.co' }
- )
- )}
-
-
-
-
-
- {i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition',
- {
- defaultMessage:
- 'Test your link with values from an example transaction document based on the filters above.',
- }
- )}
-
-
-
-
- {error && (
-
-
-
- )}
-
-
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
index ccd2b0d425743..dfe768735d19b 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
@@ -22,7 +22,7 @@ import { FiltersSection } from './FiltersSection';
import { FlyoutFooter } from './FlyoutFooter';
import { LinkSection } from './LinkSection';
import { saveCustomLink } from './saveCustomLink';
-import { LinkPreview } from './LinkPreview';
+import { LinkPreview } from './link_preview';
import { Documentation } from './Documentation';
interface Props {
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx
new file mode 100644
index 0000000000000..3bf17a733bf8a
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { ComponentProps } from 'react';
+import { CoreStart } from 'kibana/public';
+import { createCallApmApi } from '../../../../../../services/rest/createCallApmApi';
+import { LinkPreview } from './link_preview';
+
+export default {
+ title:
+ 'app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview',
+ component: LinkPreview,
+};
+
+export function Example({
+ filters,
+ label,
+ url,
+}: ComponentProps) {
+ const coreMock = ({
+ http: {
+ get: async () => ({ transaction: { id: '0' } }),
+ },
+ uiSettings: { get: () => false },
+ } as unknown) as CoreStart;
+
+ createCallApmApi(coreMock);
+
+ return ;
+}
+Example.args = {
+ filters: [],
+ label: 'Example label',
+ url: 'https://example.com',
+} as ComponentProps;
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
index 6348157104287..407f460f25ad3 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview';
+import { LinkPreview } from '../CreateEditCustomLinkFlyout/link_preview';
import {
render,
getNodeText,
@@ -14,15 +14,18 @@ import {
act,
waitFor,
} from '@testing-library/react';
-import * as apmApi from '../../../../../../services/rest/createCallApmApi';
+import {
+ getCallApmApiSpy,
+ CallApmApiSpy,
+} from '../../../../../../services/rest/callApmApiSpy';
export const removeExternalLinkText = (str: string) =>
str.replace(/\(opens in a new tab or window\)/g, '');
describe('LinkPreview', () => {
- let callApmApiSpy: jest.SpyInstance;
+ let callApmApiSpy: CallApmApiSpy;
beforeAll(() => {
- callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({
+ callApmApiSpy = getCallApmApiSpy().mockResolvedValue({
transaction: { id: 'foo' },
});
});
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx
new file mode 100644
index 0000000000000..726d4ba0d65ee
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import {
+ EuiPanel,
+ EuiText,
+ EuiSpacer,
+ EuiLink,
+ EuiToolTip,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { debounce } from 'lodash';
+import { Filter } from '../../../../../../../common/custom_link/custom_link_types';
+import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
+import { callApmApi } from '../../../../../../services/rest/createCallApmApi';
+import { replaceTemplateVariables, convertFiltersToQuery } from './helper';
+
+export interface LinkPreviewProps {
+ label: string;
+ url: string;
+ filters: Filter[];
+}
+
+const fetchTransaction = debounce(
+ async (filters: Filter[], callback: (transaction: Transaction) => void) => {
+ const transaction = await callApmApi({
+ signal: null,
+ endpoint: 'GET /api/apm/settings/custom_links/transaction',
+ params: { query: convertFiltersToQuery(filters) },
+ });
+ callback(transaction);
+ },
+ 1000
+);
+
+const getTextColor = (value?: string) => (value ? 'default' : 'subdued');
+
+export function LinkPreview({ label, url, filters }: LinkPreviewProps) {
+ const [transaction, setTransaction] = useState();
+
+ useEffect(() => {
+ /*
+ React throwns "Can't perform a React state update on an unmounted component"
+ It happens when the Custom Link flyout is closed before the return of the api request.
+ To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction.
+ */
+ let isUnmounted = false;
+ fetchTransaction(filters, (_transaction: Transaction) => {
+ if (!isUnmounted) {
+ setTransaction(_transaction);
+ }
+ });
+ return () => {
+ isUnmounted = true;
+ };
+ }, [filters]);
+
+ const { formattedUrl, error } = replaceTemplateVariables(url, transaction);
+
+ return (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.previewSectionTitle',
+ {
+ defaultMessage: 'Preview',
+ }
+ )}
+
+
+
+
+
+ {label
+ ? label
+ : i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.default.label',
+ { defaultMessage: 'Elastic.co' }
+ )}
+
+
+
+ {url ? (
+
+ {formattedUrl}
+
+ ) : (
+ i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.default.url',
+ { defaultMessage: 'https://www.elastic.co' }
+ )
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition',
+ {
+ defaultMessage:
+ 'Test your link with values from an example transaction document based on the filters above.',
+ }
+ )}
+
+
+
+
+ {error && (
+
+
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
index 77835afef863a..7d119b8c406da 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
@@ -8,6 +8,7 @@
import { fireEvent, render, RenderResult } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { getCallApmApiSpy } from '../../../../../services/rest/callApmApiSpy';
import { CustomLinkOverview } from '.';
import { License } from '../../../../../../../licensing/common/license';
import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
@@ -17,7 +18,6 @@ import {
} from '../../../../../context/apm_plugin/mock_apm_plugin_context';
import { LicenseContext } from '../../../../../context/license/license_context';
import * as hooks from '../../../../../hooks/use_fetcher';
-import * as apmApi from '../../../../../services/rest/createCallApmApi';
import {
expectTextsInDocument,
expectTextsNotInDocument,
@@ -43,7 +43,7 @@ function getMockAPMContext({ canSave }: { canSave: boolean }) {
describe('CustomLink', () => {
beforeAll(() => {
- jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({});
+ getCallApmApiSpy().mockResolvedValue({});
});
afterAll(() => {
jest.resetAllMocks();
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
index 49fa3eab47862..ab18a31e76917 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
@@ -20,7 +20,7 @@ import { INVALID_LICENSE } from '../../../../../../common/custom_link';
import { CustomLink } from '../../../../../../common/custom_link/custom_link_types';
import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher';
import { useLicenseContext } from '../../../../../context/license/use_license_context';
-import { LicensePrompt } from '../../../../shared/LicensePrompt';
+import { LicensePrompt } from '../../../../shared/license_prompt';
import { CreateCustomLinkButton } from './CreateCustomLinkButton';
import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout';
import { CustomLinkTable } from './CustomLinkTable';
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 72f0249f07bf6..62b39664cf63d 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -14,7 +14,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
import { useFetcher } from '../../../../hooks/use_fetcher';
-import { LicensePrompt } from '../../../shared/LicensePrompt';
+import { LicensePrompt } from '../../../shared/license_prompt';
import { useLicenseContext } from '../../../../context/license/use_license_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx
index e0651edbeb79b..62c547aa69e0d 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx
@@ -34,7 +34,7 @@ import {
} from '../../../../../observability/public';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import { useLicenseContext } from '../../../context/license/use_license_context';
-import { LicensePrompt } from '../../shared/LicensePrompt';
+import { LicensePrompt } from '../../shared/license_prompt';
import { IUrlParams } from '../../../context/url_params_context/types';
const latencyTab = {
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
index b30faac7a65af..c6ed4e640693f 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
@@ -22,9 +22,12 @@ import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_b
import { renderWithTheme } from '../../../utils/testHelpers';
import { ServiceOverview } from './';
import { waitFor } from '@testing-library/dom';
-import * as callApmApiModule from '../../../services/rest/createCallApmApi';
import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
+import {
+ getCallApmApiSpy,
+ getCreateCallApmApiSpy,
+} from '../../../services/rest/callApmApiSpy';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
@@ -83,10 +86,10 @@ describe('ServiceOverview', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const calls = {
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': {
- error_groups: [],
+ error_groups: [] as any[],
},
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': {
- transactionGroups: [],
+ transactionGroups: [] as any[],
totalTransactionGroups: 0,
isAggregationAccurate: true,
},
@@ -95,19 +98,17 @@ describe('ServiceOverview', () => {
};
/* eslint-enable @typescript-eslint/naming-convention */
- jest
- .spyOn(callApmApiModule, 'createCallApmApi')
- .mockImplementation(() => {});
-
- const callApmApi = jest
- .spyOn(callApmApiModule, 'callApmApi')
- .mockImplementation(({ endpoint }) => {
+ const callApmApiSpy = getCallApmApiSpy().mockImplementation(
+ ({ endpoint }) => {
const response = calls[endpoint as keyof typeof calls];
return response
? Promise.resolve(response)
: Promise.reject(`Response for ${endpoint} is not defined`);
- });
+ }
+ );
+
+ getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any);
jest
.spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown')
.mockReturnValue({
@@ -124,7 +125,7 @@ describe('ServiceOverview', () => {
);
await waitFor(() =>
- expect(callApmApi).toHaveBeenCalledTimes(Object.keys(calls).length)
+ expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length)
);
expect((await findAllByText('Latency')).length).toBeGreaterThan(0);
diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx
deleted file mode 100644
index 97a48a61e47cc..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { useKibanaUrl } from '../../../hooks/useKibanaUrl';
-
-interface Props {
- text: string;
- showBetaBadge?: boolean;
-}
-
-export function LicensePrompt({ text, showBetaBadge = false }: Props) {
- const licensePageUrl = useKibanaUrl(
- '/app/management/stack/license_management'
- );
-
- const renderLicenseBody = (
-
- {i18n.translate('xpack.apm.license.title', {
- defaultMessage: 'Start free 30-day trial',
- })}
-
- }
- body={{text}
}
- actions={
-
- {i18n.translate('xpack.apm.license.button', {
- defaultMessage: 'Start trial',
- })}
-
- }
- />
- );
-
- const renderWithBetaBadge = (
-
- {renderLicenseBody}
-
- );
-
- return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}>;
-}
diff --git a/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx
new file mode 100644
index 0000000000000..0950cff5127fc
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiButton, EuiCard, EuiTextColor } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { useKibanaUrl } from '../../../hooks/useKibanaUrl';
+
+export interface LicensePromptProps {
+ text: string;
+ showBetaBadge?: boolean;
+}
+
+export function LicensePrompt({
+ text,
+ showBetaBadge = false,
+}: LicensePromptProps) {
+ const licensePageUrl = useKibanaUrl(
+ '/app/management/stack/license_management'
+ );
+
+ return (
+ {text}}
+ footer={
+
+ {i18n.translate('xpack.apm.license.button', {
+ defaultMessage: 'Start trial',
+ })}
+
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
similarity index 61%
rename from x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx
rename to x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
index 57f782a020082..35e22b50306d9 100644
--- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx
+++ b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { ComponentType } from 'react';
+import React, { ComponentProps, ComponentType } from 'react';
import { LicensePrompt } from '.';
import {
ApmPluginContext,
@@ -17,19 +17,25 @@ const contextMock = ({
} as unknown) as ApmPluginContextValue;
export default {
- title: 'app/LicensePrompt',
+ title: 'shared/LicensePrompt',
component: LicensePrompt,
decorators: [
(Story: ComponentType) => (
- {' '}
+
),
],
};
-export function Example() {
- return (
-
- );
+export function Example({
+ showBetaBadge,
+ text,
+}: ComponentProps) {
+ return ;
}
+Example.args = {
+ showBetaBadge: false,
+ text:
+ 'To create Feature name, you must be subscribed to an Elastic X license or above.',
+} as ComponentProps;
diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
index 29fabc51fd582..00447607cf787 100644
--- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
+++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
@@ -10,10 +10,10 @@ import {
fetchObservabilityOverviewPageData,
getHasData,
} from './apm_observability_overview_fetchers';
-import * as createCallApmApi from './createCallApmApi';
+import { getCallApmApiSpy } from './callApmApiSpy';
describe('Observability dashboard data', () => {
- const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
+ const callApmApiMock = getCallApmApiSpy();
const params = {
absoluteTime: {
start: moment('2020-07-02T13:25:11.629Z').valueOf(),
@@ -84,7 +84,7 @@ describe('Observability dashboard data', () => {
callApmApiMock.mockImplementation(() =>
Promise.resolve({
serviceCount: 0,
- transactionPerMinute: { value: null, timeseries: [] },
+ transactionPerMinute: { value: null, timeseries: [] as any },
})
);
const response = await fetchObservabilityOverviewPageData(params);
diff --git a/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
new file mode 100644
index 0000000000000..ba9f740e06d0d
--- /dev/null
+++ b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as createCallApmApi from './createCallApmApi';
+import type { AbstractAPMClient } from './createCallApmApi';
+
+export type CallApmApiSpy = jest.SpyInstance<
+ Promise,
+ Parameters
+>;
+
+export type CreateCallApmApiSpy = jest.SpyInstance;
+
+export const getCreateCallApmApiSpy = () =>
+ (jest.spyOn(
+ createCallApmApi,
+ 'createCallApmApi'
+ ) as unknown) as CreateCallApmApiSpy;
+export const getCallApmApiSpy = () =>
+ (jest.spyOn(createCallApmApi, 'callApmApi') as unknown) as CallApmApiSpy;
diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
index b0cce3296fe21..0e82d70faf1e1 100644
--- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
+++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
@@ -6,30 +6,68 @@
*/
import { CoreSetup, CoreStart } from 'kibana/public';
-import { parseEndpoint } from '../../../common/apm_api/parse_endpoint';
+import * as t from 'io-ts';
+import type {
+ ClientRequestParamsOf,
+ EndpointOf,
+ ReturnOf,
+ RouteRepositoryClient,
+ ServerRouteRepository,
+ ServerRoute,
+} from '@kbn/server-route-repository';
+import { formatRequest } from '@kbn/server-route-repository/target/format_request';
import { FetchOptions } from '../../../common/fetch_options';
import { callApi } from './callApi';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import type { APMAPI } from '../../../server/routes/create_apm_api';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import type { Client } from '../../../server/routes/typings';
-
-export type APMClient = Client;
-export type AutoAbortedAPMClient = Client;
+import type {
+ APMServerRouteRepository,
+ InspectResponse,
+ APMRouteHandlerResources,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../server';
export type APMClientOptions = Omit<
FetchOptions,
'query' | 'body' | 'pathname' | 'signal'
> & {
- endpoint: string;
signal: AbortSignal | null;
- params?: {
- body?: any;
- query?: Record;
- path?: Record;
- };
};
+export type APMClient = RouteRepositoryClient<
+ APMServerRouteRepository,
+ APMClientOptions
+>;
+
+export type AutoAbortedAPMClient = RouteRepositoryClient<
+ APMServerRouteRepository,
+ Omit
+>;
+
+export type APIReturnType<
+ TEndpoint extends EndpointOf
+> = ReturnOf & {
+ _inspect?: InspectResponse;
+};
+
+export type APIEndpoint = EndpointOf;
+
+export type APIClientRequestParamsOf<
+ TEndpoint extends EndpointOf
+> = ClientRequestParamsOf;
+
+export type AbstractAPMRepository = ServerRouteRepository<
+ APMRouteHandlerResources,
+ {},
+ Record<
+ string,
+ ServerRoute
+ >
+>;
+
+export type AbstractAPMClient = RouteRepositoryClient<
+ AbstractAPMRepository,
+ APMClientOptions
+>;
+
export let callApmApi: APMClient = () => {
throw new Error(
'callApmApi has to be initialized before used. Call createCallApmApi first.'
@@ -37,9 +75,13 @@ export let callApmApi: APMClient = () => {
};
export function createCallApmApi(core: CoreStart | CoreSetup) {
- callApmApi = ((options: APMClientOptions) => {
- const { endpoint, params, ...opts } = options;
- const { method, pathname } = parseEndpoint(endpoint, params?.path);
+ callApmApi = ((options) => {
+ const { endpoint, ...opts } = options;
+ const { params } = (options as unknown) as {
+ params?: Partial>;
+ };
+
+ const { method, pathname } = formatRequest(endpoint, params?.path);
return callApi(core, {
...opts,
@@ -50,10 +92,3 @@ export function createCallApmApi(core: CoreStart | CoreSetup) {
});
}) as APMClient;
}
-
-// infer return type from API
-export type APIReturnType<
- TPath extends keyof APMAPI['_S']
-> = APMAPI['_S'][TPath] extends { ret: any }
- ? APMAPI['_S'][TPath]['ret']
- : unknown;
diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts
index 00910353ac278..9ab56c1a303ea 100644
--- a/x-pack/plugins/apm/server/index.ts
+++ b/x-pack/plugins/apm/server/index.ts
@@ -120,5 +120,9 @@ export function mergeConfigs(
export const plugin = (initContext: PluginInitializerContext) =>
new APMPlugin(initContext);
-export { APMPlugin, APMPluginSetup } from './plugin';
+export { APMPlugin } from './plugin';
+export { APMPluginSetup } from './types';
+export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository';
+export { InspectResponse, APMRouteHandlerResources } from './routes/typings';
+
export type { ProcessorEvent } from '../common/processor_event';
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
index 1f0aa401bcab0..989297544c78f 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
@@ -10,7 +10,7 @@
import { omit } from 'lodash';
import chalk from 'chalk';
import { KibanaRequest } from '../../../../../../../src/core/server';
-import { inspectableEsQueriesMap } from '../../../routes/create_api';
+import { inspectableEsQueriesMap } from '../../../routes/register_routes';
function formatObj(obj: Record) {
return JSON.stringify(obj, null, 2);
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
index 45e17c1678518..9d7434d127ead 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { KibanaRequest } from 'src/core/server';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import {
CreateIndexRequest,
@@ -13,7 +12,7 @@ import {
IndexRequest,
} from '@elastic/elasticsearch/api/types';
import { unwrapEsResponse } from '../../../../../../observability/server';
-import { APMRequestHandlerContext } from '../../../../routes/typings';
+import { APMRouteHandlerResources } from '../../../../routes/typings';
import {
ESSearchResponse,
ESSearchRequest,
@@ -31,11 +30,9 @@ export type APMInternalClient = ReturnType;
export function createInternalESClient({
context,
+ debug,
request,
-}: {
- context: APMRequestHandlerContext;
- request: KibanaRequest;
-}) {
+}: Pick & { debug: boolean }) {
const { asInternalUser } = context.core.elasticsearch.client;
function callEs({
@@ -53,7 +50,7 @@ export function createInternalESClient({
title: getDebugTitle(request),
body: getDebugBody(params, requestType),
}),
- debug: context.params.query._inspect,
+ debug,
isCalledWithInternalUser: true,
request,
requestType,
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
index c0707d0286180..c0ff0cab88f47 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
@@ -7,8 +7,7 @@
import { setupRequest } from './setup_request';
import { APMConfig } from '../..';
-import { APMRequestHandlerContext } from '../../routes/typings';
-import { KibanaRequest } from '../../../../../../src/core/server';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { ProcessorEvent } from '../../../common/processor_event';
import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
@@ -32,7 +31,7 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({
},
}));
-function getMockRequest() {
+function getMockResources() {
const esClientMock = {
asCurrentUser: {
search: jest.fn().mockResolvedValue({ body: {} }),
@@ -42,7 +41,7 @@ function getMockRequest() {
},
};
- const mockContext = ({
+ const mockResources = ({
config: new Proxy(
{},
{
@@ -54,65 +53,69 @@ function getMockRequest() {
_inspect: false,
},
},
- core: {
- elasticsearch: {
- client: esClientMock,
- },
- uiSettings: {
- client: {
- get: jest.fn().mockResolvedValue(false),
+ context: {
+ core: {
+ elasticsearch: {
+ client: esClientMock,
},
- },
- savedObjects: {
- client: {
- get: jest.fn(),
+ uiSettings: {
+ client: {
+ get: jest.fn().mockResolvedValue(false),
+ },
+ },
+ savedObjects: {
+ client: {
+ get: jest.fn(),
+ },
},
},
},
plugins: {
ml: undefined,
},
- } as unknown) as APMRequestHandlerContext & {
- core: {
- elasticsearch: {
- client: typeof esClientMock;
- };
- uiSettings: {
- client: {
- get: jest.Mock;
+ request: {
+ url: '',
+ events: {
+ aborted$: {
+ subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
+ },
+ },
+ },
+ } as unknown) as APMRouteHandlerResources & {
+ context: {
+ core: {
+ elasticsearch: {
+ client: typeof esClientMock;
};
- };
- savedObjects: {
- client: {
- get: jest.Mock;
+ uiSettings: {
+ client: {
+ get: jest.Mock;
+ };
+ };
+ savedObjects: {
+ client: {
+ get: jest.Mock;
+ };
};
};
};
};
- const mockRequest = ({
- url: '',
- events: {
- aborted$: {
- subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
- },
- },
- } as unknown) as KibanaRequest;
-
- return { mockContext, mockRequest };
+ return mockResources;
}
describe('setupRequest', () => {
describe('with default args', () => {
it('calls callWithRequest', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: { events: [ProcessorEvent.transaction] },
body: { foo: 'bar' },
});
+
expect(
- mockContext.core.elasticsearch.client.asCurrentUser.search
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
).toHaveBeenCalledWith({
index: ['apm-*'],
body: {
@@ -132,14 +135,14 @@ describe('setupRequest', () => {
});
it('calls callWithInternalUser', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { internalClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { internalClient } = await setupRequest(mockResources);
await internalClient.search({
index: ['apm-*'],
body: { foo: 'bar' },
} as any);
expect(
- mockContext.core.elasticsearch.client.asInternalUser.search
+ mockResources.context.core.elasticsearch.client.asInternalUser.search
).toHaveBeenCalledWith({
index: ['apm-*'],
body: {
@@ -151,8 +154,8 @@ describe('setupRequest', () => {
describe('with a bool filter', () => {
it('adds a range filter for `observer.version_major` to the existing filter', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
events: [ProcessorEvent.transaction],
@@ -162,8 +165,8 @@ describe('setupRequest', () => {
},
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
- .calls[0][0];
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
+ .mock.calls[0][0];
expect(params.body).toEqual({
query: {
bool: {
@@ -178,8 +181,8 @@ describe('setupRequest', () => {
});
it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search(
{
apm: {
@@ -194,8 +197,8 @@ describe('setupRequest', () => {
}
);
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
- .calls[0][0];
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
+ .mock.calls[0][0];
expect(params.body).toEqual({
query: {
bool: {
@@ -216,15 +219,15 @@ describe('setupRequest', () => {
describe('without a bool filter', () => {
it('adds a range filter for `observer.version_major`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
events: [ProcessorEvent.error],
},
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.body).toEqual({
query: {
@@ -241,12 +244,12 @@ describe('without a bool filter', () => {
describe('with includeFrozen=false', () => {
it('sets `ignore_throttled=true`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
+ const mockResources = getMockResources();
// mock includeFrozen to return false
- mockContext.core.uiSettings.client.get.mockResolvedValue(false);
+ mockResources.context.core.uiSettings.client.get.mockResolvedValue(false);
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
@@ -255,7 +258,7 @@ describe('with includeFrozen=false', () => {
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.ignore_throttled).toBe(true);
});
@@ -263,19 +266,19 @@ describe('with includeFrozen=false', () => {
describe('with includeFrozen=true', () => {
it('sets `ignore_throttled=false`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
+ const mockResources = getMockResources();
// mock includeFrozen to return true
- mockContext.core.uiSettings.client.get.mockResolvedValue(true);
+ mockResources.context.core.uiSettings.client.get.mockResolvedValue(true);
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: { events: [] },
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.ignore_throttled).toBe(false);
});
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index fff661250c6df..40836cb6635e3 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -11,7 +11,7 @@ import { APMConfig } from '../..';
import { KibanaRequest } from '../../../../../../src/core/server';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
import { UIFilters } from '../../../typings/ui_filters';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import {
ApmIndicesConfig,
getApmIndices,
@@ -44,7 +44,7 @@ export interface SetupTimeRange {
}
interface SetupRequestParams {
- query?: {
+ query: {
_inspect?: boolean;
/**
@@ -64,13 +64,19 @@ type InferSetup = Setup &
(TParams extends { query: { start: number } } ? { start: number } : {}) &
(TParams extends { query: { end: number } } ? { end: number } : {});
-export async function setupRequest(
- context: APMRequestHandlerContext,
- request: KibanaRequest
-): Promise> {
+export async function setupRequest({
+ context,
+ params,
+ core,
+ plugins,
+ request,
+ config,
+ logger,
+}: APMRouteHandlerResources & {
+ params: TParams;
+}): Promise> {
return withApmSpan('setup_request', async () => {
- const { config, logger } = context;
- const { query } = context.params;
+ const { query } = params;
const [indices, includeFrozen] = await Promise.all([
getApmIndices({
@@ -88,7 +94,7 @@ export async function setupRequest(
indices,
apmEventClient: createApmEventClient({
esClient: context.core.elasticsearch.client.asCurrentUser,
- debug: context.params.query._inspect,
+ debug: query._inspect,
request,
indices,
options: { includeFrozen },
@@ -96,11 +102,12 @@ export async function setupRequest(
internalClient: createInternalESClient({
context,
request,
+ debug: query._inspect,
}),
ml:
- context.plugins.ml && isActivePlatinumLicense(context.licensing.license)
+ plugins.ml && isActivePlatinumLicense(context.licensing.license)
? getMlSetup(
- context.plugins.ml,
+ plugins.ml.setup,
context.core.savedObjects.client,
request
)
@@ -118,8 +125,8 @@ export async function setupRequest(
}
function getMlSetup(
- ml: Required['ml'],
- savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'],
+ ml: Required['ml']['setup'],
+ savedObjectsClient: APMRouteHandlerResources['context']['core']['savedObjects']['client'],
request: KibanaRequest
) {
return {
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
index 19163da449b90..a5340c1220b44 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
@@ -8,21 +8,9 @@
import { createStaticIndexPattern } from './create_static_index_pattern';
import { Setup } from '../helpers/setup_request';
import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data';
-import { APMRequestHandlerContext } from '../../routes/typings';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
+import { APMConfig } from '../..';
-function getMockContext(config: Record) {
- return ({
- config,
- core: {
- savedObjects: {
- client: {
- create: jest.fn(),
- },
- },
- },
- } as unknown) as APMRequestHandlerContext;
-}
function getMockSavedObjectsClient() {
return ({
create: jest.fn(),
@@ -32,13 +20,13 @@ function getMockSavedObjectsClient() {
describe('createStaticIndexPattern', () => {
it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': false,
- });
+
const savedObjectsClient = getMockSavedObjectsClient();
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': false,
+ } as APMConfig,
savedObjectsClient,
'default'
);
@@ -47,9 +35,6 @@ describe('createStaticIndexPattern', () => {
it(`should not create index pattern if no APM data is found`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': true,
- });
// does not have APM data
jest
@@ -60,7 +45,9 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': true,
+ } as APMConfig,
savedObjectsClient,
'default'
);
@@ -69,9 +56,6 @@ describe('createStaticIndexPattern', () => {
it(`should create index pattern`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': true,
- });
// does have APM data
jest
@@ -82,7 +66,9 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': true,
+ } as APMConfig,
savedObjectsClient,
'default'
);
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
index b91fb8342a212..e627e9ed1d6cf 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
@@ -12,20 +12,18 @@ import {
} from '../../../../../../src/plugins/apm_oss/server';
import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data';
import { Setup } from '../helpers/setup_request';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js';
import { withApmSpan } from '../../utils/with_apm_span';
import { getApmIndexPatternTitle } from './get_apm_index_pattern_title';
export async function createStaticIndexPattern(
setup: Setup,
- context: APMRequestHandlerContext,
+ config: APMRouteHandlerResources['config'],
savedObjectsClient: InternalSavedObjectsClient,
spaceId: string | undefined
): Promise {
return withApmSpan('create_static_index_pattern', async () => {
- const { config } = context;
-
// don't autocreate APM index pattern if it's been disabled via the config
if (!config['xpack.apm.autocreateApmIndexPattern']) {
return false;
@@ -39,7 +37,7 @@ export async function createStaticIndexPattern(
}
try {
- const apmIndexPatternTitle = getApmIndexPatternTitle(context);
+ const apmIndexPatternTitle = getApmIndexPatternTitle(config);
await withApmSpan('create_index_pattern_saved_object', () =>
savedObjectsClient.create(
'index-pattern',
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
index 41abe82de8ff2..faec64c798c7d 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
-export function getApmIndexPatternTitle(context: APMRequestHandlerContext) {
- return context.config['apm_oss.indexPattern'];
+export function getApmIndexPatternTitle(
+ config: APMRouteHandlerResources['config']
+) {
+ return config['apm_oss.indexPattern'];
}
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
index 5d5e6eebb4c9f..8bbc22fbf289d 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
@@ -9,7 +9,7 @@ import {
IndexPatternsFetcher,
FieldDescriptor,
} from '../../../../../../src/plugins/data/server';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { withApmSpan } from '../../utils/with_apm_span';
export interface IndexPatternTitleAndFields {
@@ -20,12 +20,12 @@ export interface IndexPatternTitleAndFields {
// TODO: this is currently cached globally. In the future we might want to cache this per user
export const getDynamicIndexPattern = ({
+ config,
context,
-}: {
- context: APMRequestHandlerContext;
-}) => {
+ logger,
+}: Pick) => {
return withApmSpan('get_dynamic_index_pattern', async () => {
- const indexPatternTitle = context.config['apm_oss.indexPattern'];
+ const indexPatternTitle = config['apm_oss.indexPattern'];
const indexPatternsFetcher = new IndexPatternsFetcher(
context.core.elasticsearch.client.asCurrentUser
@@ -50,7 +50,7 @@ export const getDynamicIndexPattern = ({
} catch (e) {
const notExists = e.output?.statusCode === 404;
if (notExists) {
- context.logger.error(
+ logger.error(
`Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist`
);
return;
diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
index a1587611b0a2a..d8dbc242986a6 100644
--- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
@@ -14,7 +14,7 @@ import {
APM_INDICES_SAVED_OBJECT_ID,
} from '../../../../common/apm_saved_object_constants';
import { APMConfig } from '../../..';
-import { APMRequestHandlerContext } from '../../../routes/typings';
+import { APMRouteHandlerResources } from '../../../routes/typings';
import { withApmSpan } from '../../../utils/with_apm_span';
type ISavedObjectsClient = Pick;
@@ -91,9 +91,8 @@ const APM_UI_INDICES: ApmIndicesName[] = [
export async function getApmIndexSettings({
context,
-}: {
- context: APMRequestHandlerContext;
-}) {
+ config,
+}: Pick) {
let apmIndicesSavedObject: PromiseReturnType;
try {
apmIndicesSavedObject = await getApmIndicesSavedObject(
@@ -106,7 +105,7 @@ export async function getApmIndexSettings({
throw error;
}
}
- const apmIndicesConfig = getApmIndicesConfig(context.config);
+ const apmIndicesConfig = getApmIndicesConfig(config);
return APM_UI_INDICES.map((configurationName) => ({
configurationName,
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index db96794627519..074df7eaafd3c 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { combineLatest, Observable } from 'rxjs';
+import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators';
import {
CoreSetup,
@@ -16,22 +16,10 @@ import {
Plugin,
PluginInitializerContext,
} from 'src/core/server';
-import { SpacesPluginSetup } from '../../spaces/server';
+import { mapValues } from 'lodash';
import { APMConfig, APMXPackConfig } from '.';
import { mergeConfigs } from './index';
-import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
-import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
-import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
-import { ActionsPlugin } from '../../actions/server';
-import { AlertingPlugin } from '../../alerting/server';
-import { CloudSetup } from '../../cloud/server';
-import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
-import { LicensingPluginSetup } from '../../licensing/server';
-import { MlPluginSetup } from '../../ml/server';
-import { ObservabilityPluginSetup } from '../../observability/server';
-import { SecurityPluginSetup } from '../../security/server';
-import { TaskManagerSetupContract } from '../../task_manager/server';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
import { createApmTelemetry } from './lib/apm_telemetry';
@@ -40,23 +28,29 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index';
-import { createApmApi } from './routes/create_apm_api';
import { apmIndices, apmTelemetry } from './saved_objects';
import { createElasticCloudInstructions } from './tutorial/elastic_cloud';
import { uiSettings } from './ui_settings';
-import type { ApmPluginRequestHandlerContext } from './routes/typings';
-
-export interface APMPluginSetup {
- config$: Observable;
- getApmIndices: () => ReturnType;
- createApmEventClient: (params: {
- debug?: boolean;
- request: KibanaRequest;
- context: ApmPluginRequestHandlerContext;
- }) => Promise>;
-}
-
-export class APMPlugin implements Plugin {
+import type {
+ ApmPluginRequestHandlerContext,
+ APMRouteHandlerResources,
+} from './routes/typings';
+import {
+ APMPluginSetup,
+ APMPluginSetupDependencies,
+ APMPluginStartDependencies,
+} from './types';
+import { registerRoutes } from './routes/register_routes';
+import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository';
+
+export class APMPlugin
+ implements
+ Plugin<
+ APMPluginSetup,
+ void,
+ APMPluginSetupDependencies,
+ APMPluginStartDependencies
+ > {
private currentConfig?: APMConfig;
private logger?: Logger;
constructor(private readonly initContext: PluginInitializerContext) {
@@ -64,22 +58,8 @@ export class APMPlugin implements Plugin {
}
public setup(
- core: CoreSetup,
- plugins: {
- spaces?: SpacesPluginSetup;
- apmOss: APMOSSPluginSetup;
- home: HomeServerPluginSetup;
- licensing: LicensingPluginSetup;
- cloud?: CloudSetup;
- usageCollection?: UsageCollectionSetup;
- taskManager?: TaskManagerSetupContract;
- alerting?: AlertingPlugin['setup'];
- actions?: ActionsPlugin['setup'];
- observability?: ObservabilityPluginSetup;
- features: FeaturesPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
- }
+ core: CoreSetup,
+ plugins: Omit
) {
this.logger = this.initContext.logger.get();
const config$ = this.initContext.config.create();
@@ -101,11 +81,13 @@ export class APMPlugin implements Plugin {
});
}
- this.currentConfig = mergeConfigs(
+ const currentConfig = mergeConfigs(
plugins.apmOss.config,
this.initContext.config.get()
);
+ this.currentConfig = currentConfig;
+
if (
plugins.taskManager &&
plugins.usageCollection &&
@@ -122,8 +104,8 @@ export class APMPlugin implements Plugin {
}
const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider();
- plugins.home.tutorials.unregisterTutorial(ossTutorialProvider);
- plugins.home.tutorials.registerTutorial(() => {
+ plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider);
+ plugins.home?.tutorials.registerTutorial(() => {
const ossPart = ossTutorialProvider({});
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
ossPart.artifacts.application = {
@@ -147,10 +129,26 @@ export class APMPlugin implements Plugin {
registerFeaturesUsage({ licensingPlugin: plugins.licensing });
- createApmApi().init(core, {
- config$: mergedConfig$,
- logger: this.logger!,
- plugins,
+ registerRoutes({
+ core: {
+ setup: core,
+ start: () => core.getStartServices().then(([coreStart]) => coreStart),
+ },
+ logger: this.logger,
+ config: currentConfig,
+ repository: getGlobalApmServerRouteRepository(),
+ plugins: mapValues(plugins, (value, key) => {
+ return {
+ setup: value,
+ start: () =>
+ core.getStartServices().then((services) => {
+ const [, pluginsStartContracts] = services;
+ return pluginsStartContracts[
+ key as keyof APMPluginStartDependencies
+ ];
+ }),
+ };
+ }) as APMRouteHandlerResources['plugins'],
});
const boundGetApmIndices = async () =>
diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
index 3bebcd49ec34a..0175860e93d35 100644
--- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
@@ -10,7 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ
import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count';
import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate';
import { setupRequest } from '../../lib/helpers/setup_request';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
import { rangeRt } from '../default_api_types';
const alertParamsRt = t.intersection([
@@ -29,13 +30,14 @@ const alertParamsRt = t.intersection([
export type AlertParams = t.TypeOf;
-export const transactionErrorRateChartPreview = createRoute({
+const transactionErrorRateChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { _inspect, ...alertParams } = params.query;
const errorRateChartPreview = await getTransactionErrorRateChartPreview({
setup,
@@ -46,13 +48,16 @@ export const transactionErrorRateChartPreview = createRoute({
},
});
-export const transactionErrorCountChartPreview = createRoute({
+const transactionErrorCountChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { _inspect, ...alertParams } = params.query;
+
const errorCountChartPreview = await getTransactionErrorCountChartPreview({
setup,
alertParams,
@@ -62,13 +67,16 @@ export const transactionErrorCountChartPreview = createRoute({
},
});
-export const transactionDurationChartPreview = createRoute({
+const transactionDurationChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
+
+ const { _inspect, ...alertParams } = params.query;
const latencyChartPreview = await getTransactionDurationChartPreview({
alertParams,
@@ -78,3 +86,9 @@ export const transactionDurationChartPreview = createRoute({
return { latencyChartPreview };
},
});
+
+export const alertsChartPreviewRouteRepository = createApmServerRouteRepository()
+ .add(transactionErrorRateChartPreview)
+ .add(transactionDurationChartPreview)
+ .add(transactionErrorCountChartPreview)
+ .add(transactionDurationChartPreview);
diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts
index c7c69e0774822..4728aa2e8d3f6 100644
--- a/x-pack/plugins/apm/server/routes/correlations.ts
+++ b/x-pack/plugins/apm/server/routes/correlations.ts
@@ -14,7 +14,8 @@ import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overal
import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions';
import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution';
import { setupRequest } from '../lib/helpers/setup_request';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
const INVALID_LICENSE = i18n.translate(
@@ -25,7 +26,7 @@ const INVALID_LICENSE = i18n.translate(
}
);
-export const correlationsLatencyDistributionRoute = createRoute({
+const correlationsLatencyDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/latency/overall_distribution',
params: t.type({
query: t.intersection([
@@ -40,18 +41,19 @@ export const correlationsLatencyDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
serviceName,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
return getOverallLatencyDistribution({
environment,
@@ -64,7 +66,7 @@ export const correlationsLatencyDistributionRoute = createRoute({
},
});
-export const correlationsForSlowTransactionsRoute = createRoute({
+const correlationsForSlowTransactionsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/latency/slow_transactions',
params: t.type({
query: t.intersection([
@@ -85,11 +87,13 @@ export const correlationsForSlowTransactionsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
+
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
@@ -100,7 +104,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
fieldNames,
maxLatency,
distributionInterval,
- } = context.params.query;
+ } = params.query;
return getCorrelationsForSlowTransactions({
environment,
@@ -117,7 +121,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
},
});
-export const correlationsErrorDistributionRoute = createRoute({
+const correlationsErrorDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/errors/overall_timeseries',
params: t.type({
query: t.intersection([
@@ -132,18 +136,20 @@ export const correlationsErrorDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, context } = resources;
+
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
serviceName,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
return getOverallErrorTimeseries({
environment,
@@ -156,7 +162,7 @@ export const correlationsErrorDistributionRoute = createRoute({
},
});
-export const correlationsForFailedTransactionsRoute = createRoute({
+const correlationsForFailedTransactionsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/errors/failed_transactions',
params: t.type({
query: t.intersection([
@@ -174,11 +180,12 @@ export const correlationsForFailedTransactionsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
@@ -186,7 +193,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({
transactionType,
transactionName,
fieldNames,
- } = context.params.query;
+ } = params.query;
return getCorrelationsForFailedTransactions({
environment,
@@ -199,3 +206,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({
});
},
});
+
+export const correlationsRouteRepository = createApmServerRouteRepository()
+ .add(correlationsLatencyDistributionRoute)
+ .add(correlationsForSlowTransactionsRoute)
+ .add(correlationsErrorDistributionRoute)
+ .add(correlationsForFailedTransactionsRoute);
diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts
deleted file mode 100644
index 9958b8dec0124..0000000000000
--- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import * as t from 'io-ts';
-import { createApi } from './index';
-import { CoreSetup, Logger } from 'src/core/server';
-import { RouteParamsRT } from '../typings';
-import { BehaviorSubject } from 'rxjs';
-import { APMConfig } from '../..';
-import { jsonRt } from '../../../common/runtime_types/json_rt';
-
-const getCoreMock = () => {
- const get = jest.fn();
- const post = jest.fn();
- const put = jest.fn();
- const createRouter = jest.fn().mockReturnValue({
- get,
- post,
- put,
- });
-
- const mock = {} as CoreSetup;
-
- return {
- mock: {
- ...mock,
- http: {
- ...mock.http,
- createRouter,
- },
- },
- get,
- post,
- put,
- createRouter,
- context: {
- measure: () => undefined,
- config$: new BehaviorSubject({} as APMConfig),
- logger: ({
- error: jest.fn(),
- } as unknown) as Logger,
- plugins: {},
- },
- };
-};
-
-const initApi = (params?: RouteParamsRT) => {
- const { mock, context, createRouter, get, post } = getCoreMock();
- const handlerMock = jest.fn();
- createApi()
- .add(() => ({
- endpoint: 'GET /foo',
- params,
- options: { tags: ['access:apm'] },
- handler: handlerMock,
- }))
- .init(mock, context);
-
- const routeHandler = get.mock.calls[0][1];
-
- const responseMock = {
- ok: jest.fn(),
- custom: jest.fn(),
- };
-
- const simulateRequest = (requestMock: any) => {
- return routeHandler(
- {},
- {
- // stub default values
- params: {},
- query: {},
- body: null,
- ...requestMock,
- },
- responseMock
- );
- };
-
- return {
- simulateRequest,
- handlerMock,
- createRouter,
- get,
- post,
- responseMock,
- };
-};
-
-describe('createApi', () => {
- it('registers a route with the server', () => {
- const { mock, context, createRouter, post, get, put } = getCoreMock();
-
- createApi()
- .add(() => ({
- endpoint: 'GET /foo',
- options: { tags: ['access:apm'] },
- handler: async () => ({}),
- }))
- .add(() => ({
- endpoint: 'POST /bar',
- params: t.type({
- body: t.string,
- }),
- options: { tags: ['access:apm'] },
- handler: async () => ({}),
- }))
- .add(() => ({
- endpoint: 'PUT /baz',
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- handler: async () => ({}),
- }))
- .add({
- endpoint: 'GET /qux',
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- handler: async () => ({}),
- })
- .init(mock, context);
-
- expect(createRouter).toHaveBeenCalledTimes(1);
-
- expect(get).toHaveBeenCalledTimes(2);
- expect(post).toHaveBeenCalledTimes(1);
- expect(put).toHaveBeenCalledTimes(1);
-
- expect(get.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm'],
- },
- path: '/foo',
- validate: expect.anything(),
- });
-
- expect(get.mock.calls[1][0]).toEqual({
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- path: '/qux',
- validate: expect.anything(),
- });
-
- expect(post.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm'],
- },
- path: '/bar',
- validate: expect.anything(),
- });
-
- expect(put.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- path: '/baz',
- validate: expect.anything(),
- });
- });
-
- describe('when validating', () => {
- describe('_inspect', () => {
- it('allows _inspect=true', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi();
- await simulateRequest({ query: { _inspect: 'true' } });
-
- const params = handlerMock.mock.calls[0][0].context.params;
- expect(params).toEqual({ query: { _inspect: true } });
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- // responds with ok
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(responseMock.ok).toHaveBeenCalledWith({
- body: { _inspect: [] },
- });
- });
-
- it('rejects _inspect=1', async () => {
- const { simulateRequest, responseMock } = initApi();
- await simulateRequest({ query: { _inspect: 1 } });
-
- // responds with error handler
- expect(responseMock.ok).not.toHaveBeenCalled();
- expect(responseMock.custom).toHaveBeenCalledWith({
- body: {
- attributes: { _inspect: [] },
- message:
- 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
- },
- statusCode: 400,
- });
- });
-
- it('allows omitting _inspect', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi();
- await simulateRequest({ query: {} });
-
- const params = handlerMock.mock.calls[0][0].context.params;
- expect(params).toEqual({ query: { _inspect: false } });
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- // responds with ok
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(responseMock.ok).toHaveBeenCalledWith({ body: {} });
- });
- });
-
- it('throws if unknown parameters are provided', async () => {
- const { simulateRequest, responseMock } = initApi();
-
- await simulateRequest({
- query: { _inspect: true, extra: '' },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
-
- await simulateRequest({
- body: { foo: 'bar' },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(2);
-
- await simulateRequest({
- params: {
- foo: 'bar',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(3);
- });
-
- it('validates path parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- path: t.type({
- foo: t.string,
- }),
- })
- );
-
- await simulateRequest({
- params: {
- foo: 'bar',
- },
- });
-
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
- expect(responseMock.custom).not.toHaveBeenCalled();
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- path: {
- foo: 'bar',
- },
- query: {
- _inspect: false,
- },
- });
-
- await simulateRequest({
- params: {
- bar: 'foo',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
-
- await simulateRequest({
- params: {
- foo: 9,
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(2);
-
- await simulateRequest({
- params: {
- foo: 'bar',
- extra: '',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(3);
- });
-
- it('validates body parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- body: t.string,
- })
- );
-
- await simulateRequest({
- body: '',
- });
-
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(handlerMock).toHaveBeenCalledTimes(1);
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- body: '',
- query: {
- _inspect: false,
- },
- });
-
- await simulateRequest({
- body: null,
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
- });
-
- it('validates query parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- query: t.type({
- bar: t.string,
- filterNames: jsonRt.pipe(t.array(t.string)),
- }),
- })
- );
-
- await simulateRequest({
- query: {
- bar: '',
- _inspect: 'true',
- filterNames: JSON.stringify(['hostName', 'agentName']),
- },
- });
-
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(handlerMock).toHaveBeenCalledTimes(1);
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- query: {
- bar: '',
- _inspect: true,
- filterNames: ['hostName', 'agentName'],
- },
- });
-
- await simulateRequest({
- query: {
- bar: '',
- foo: '',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts
deleted file mode 100644
index 87bc97d346984..0000000000000
--- a/x-pack/plugins/apm/server/routes/create_api/index.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash';
-import Boom from '@hapi/boom';
-import { schema } from '@kbn/config-schema';
-import * as t from 'io-ts';
-import { PathReporter } from 'io-ts/lib/PathReporter';
-import { isLeft } from 'fp-ts/lib/Either';
-import { KibanaRequest, RouteRegistrar } from 'src/core/server';
-import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
-import agent from 'elastic-apm-node';
-import { parseMethod } from '../../../common/apm_api/parse_endpoint';
-import { merge } from '../../../common/runtime_types/merge';
-import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt';
-import { APMConfig } from '../..';
-import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings';
-import { jsonRt } from '../../../common/runtime_types/json_rt';
-import type { ApmPluginRequestHandlerContext } from '../typings';
-
-const inspectRt = t.exact(
- t.partial({
- query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
- })
-);
-
-type RouteOrRouteFactoryFn = Parameters['add']>[0];
-
-const isNotEmpty = (val: any) =>
- val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val));
-
-export const inspectableEsQueriesMap = new WeakMap<
- KibanaRequest,
- InspectResponse
->();
-
-export function createApi() {
- const routes: RouteOrRouteFactoryFn[] = [];
- const api: ServerAPI<{}> = {
- _S: {},
- add(route) {
- routes.push((route as unknown) as RouteOrRouteFactoryFn);
- return this as any;
- },
- init(core, { config$, logger, plugins }) {
- const router = core.http.createRouter();
-
- let config = {} as APMConfig;
-
- config$.subscribe((val) => {
- config = val;
- });
-
- routes.forEach((routeOrFactoryFn) => {
- const route =
- typeof routeOrFactoryFn === 'function'
- ? routeOrFactoryFn(core)
- : routeOrFactoryFn;
-
- const { params, endpoint, options, handler } = route;
-
- const [method, path] = endpoint.split(' ');
- const typedRouterMethod = parseMethod(method);
-
- // For all runtime types with props, we create an exact
- // version that will strip all keys that are unvalidated.
- const anyObject = schema.object({}, { unknowns: 'allow' });
-
- (router[typedRouterMethod] as RouteRegistrar<
- typeof typedRouterMethod,
- ApmPluginRequestHandlerContext
- >)(
- {
- path,
- options,
- validate: {
- // `body` can be null, but `validate` expects non-nullable types
- // if any validation is defined. Not having validation currently
- // means we don't get the payload. See
- // https://github.com/elastic/kibana/issues/50179
- body: schema.nullable(anyObject),
- params: anyObject,
- query: anyObject,
- },
- },
- async (context, request, response) => {
- if (agent.isStarted()) {
- agent.addLabels({
- plugin: 'apm',
- });
- }
-
- // init debug queries
- inspectableEsQueriesMap.set(request, []);
-
- try {
- const validParams = validateParams(request, params);
- const data = await handler({
- request,
- context: {
- ...context,
- plugins,
- params: validParams,
- config,
- logger,
- },
- });
-
- const body = { ...data };
- if (validParams.query._inspect) {
- body._inspect = inspectableEsQueriesMap.get(request);
- }
-
- // cleanup
- inspectableEsQueriesMap.delete(request);
-
- return response.ok({ body });
- } catch (error) {
- logger.error(error);
- const opts = {
- statusCode: 500,
- body: {
- message: error.message,
- attributes: {
- _inspect: inspectableEsQueriesMap.get(request),
- },
- },
- };
-
- if (Boom.isBoom(error)) {
- opts.statusCode = error.output.statusCode;
- }
-
- if (error instanceof RequestAbortedError) {
- opts.statusCode = 499;
- opts.body.message = 'Client closed request';
- }
-
- return response.custom(opts);
- }
- }
- );
- });
- },
- };
-
- return api;
-}
-
-function validateParams(
- request: KibanaRequest,
- params: RouteParamsRT | undefined
-) {
- const paramsRt = params ? merge([params, inspectRt]) : inspectRt;
- const paramMap = pickBy(
- {
- path: request.params,
- body: request.body,
- query: {
- _inspect: 'false',
- // @ts-ignore
- ...request.query,
- },
- },
- isNotEmpty
- );
-
- const result = strictKeysRt(paramsRt).decode(paramMap);
-
- if (isLeft(result)) {
- throw Boom.badRequest(PathReporter.report(result)[0]);
- }
-
- // Only return values for parameters that have runtime types,
- // but always include query as _inspect is always set even if
- // it's not defined in the route.
- return mergeLodash(
- { query: { _inspect: false } },
- pickBy(result.right, isNotEmpty)
- );
-}
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
deleted file mode 100644
index 5b74aa4347f14..0000000000000
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- staticIndexPatternRoute,
- dynamicIndexPatternRoute,
- apmIndexPatternTitleRoute,
-} from './index_pattern';
-import { createApi } from './create_api';
-import { environmentsRoute } from './environments';
-import {
- errorDistributionRoute,
- errorGroupsRoute,
- errorsRoute,
-} from './errors';
-import {
- serviceAgentNameRoute,
- serviceTransactionTypesRoute,
- servicesRoute,
- serviceNodeMetadataRoute,
- serviceAnnotationsRoute,
- serviceAnnotationsCreateRoute,
- serviceErrorGroupsPrimaryStatisticsRoute,
- serviceErrorGroupsComparisonStatisticsRoute,
- serviceThroughputRoute,
- serviceDependenciesRoute,
- serviceMetadataDetailsRoute,
- serviceMetadataIconsRoute,
- serviceInstancesPrimaryStatisticsRoute,
- serviceInstancesComparisonStatisticsRoute,
- serviceProfilingStatisticsRoute,
- serviceProfilingTimelineRoute,
-} from './services';
-import {
- agentConfigurationRoute,
- getSingleAgentConfigurationRoute,
- agentConfigurationSearchRoute,
- deleteAgentConfigurationRoute,
- listAgentConfigurationEnvironmentsRoute,
- listAgentConfigurationServicesRoute,
- createOrUpdateAgentConfigurationRoute,
- agentConfigurationAgentNameRoute,
-} from './settings/agent_configuration';
-import {
- apmIndexSettingsRoute,
- apmIndicesRoute,
- saveApmIndicesRoute,
-} from './settings/apm_indices';
-import { metricsChartsRoute } from './metrics';
-import { serviceNodesRoute } from './service_nodes';
-import {
- tracesRoute,
- tracesByIdRoute,
- rootTransactionByTraceIdRoute,
-} from './traces';
-import {
- correlationsLatencyDistributionRoute,
- correlationsForSlowTransactionsRoute,
- correlationsErrorDistributionRoute,
- correlationsForFailedTransactionsRoute,
-} from './correlations';
-import {
- transactionChartsBreakdownRoute,
- transactionChartsDistributionRoute,
- transactionChartsErrorRateRoute,
- transactionGroupsRoute,
- transactionGroupsPrimaryStatisticsRoute,
- transactionLatencyChartsRoute,
- transactionThroughputChartsRoute,
- transactionGroupsComparisonStatisticsRoute,
-} from './transactions';
-import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
-import {
- createCustomLinkRoute,
- updateCustomLinkRoute,
- deleteCustomLinkRoute,
- listCustomLinksRoute,
- customLinkTransactionRoute,
-} from './settings/custom_link';
-import {
- observabilityOverviewHasDataRoute,
- observabilityOverviewRoute,
-} from './observability_overview';
-import {
- anomalyDetectionJobsRoute,
- createAnomalyDetectionJobsRoute,
- anomalyDetectionEnvironmentsRoute,
-} from './settings/anomaly_detection';
-import {
- rumHasDataRoute,
- rumClientMetricsRoute,
- rumJSErrors,
- rumLongTaskMetrics,
- rumOverviewLocalFiltersRoute,
- rumPageLoadDistBreakdownRoute,
- rumPageLoadDistributionRoute,
- rumPageViewsTrendRoute,
- rumServicesRoute,
- rumUrlSearch,
- rumVisitorsBreakdownRoute,
- rumWebCoreVitals,
-} from './rum_client';
-import {
- transactionErrorRateChartPreview,
- transactionErrorCountChartPreview,
- transactionDurationChartPreview,
-} from './alerts/chart_preview';
-
-const createApmApi = () => {
- const api = createApi()
- // index pattern
- .add(staticIndexPatternRoute)
- .add(dynamicIndexPatternRoute)
- .add(apmIndexPatternTitleRoute)
-
- // Environments
- .add(environmentsRoute)
-
- // Errors
- .add(errorDistributionRoute)
- .add(errorGroupsRoute)
- .add(errorsRoute)
-
- // Services
- .add(serviceAgentNameRoute)
- .add(serviceTransactionTypesRoute)
- .add(servicesRoute)
- .add(serviceNodeMetadataRoute)
- .add(serviceAnnotationsRoute)
- .add(serviceAnnotationsCreateRoute)
- .add(serviceErrorGroupsPrimaryStatisticsRoute)
- .add(serviceThroughputRoute)
- .add(serviceDependenciesRoute)
- .add(serviceMetadataDetailsRoute)
- .add(serviceMetadataIconsRoute)
- .add(serviceInstancesPrimaryStatisticsRoute)
- .add(serviceInstancesComparisonStatisticsRoute)
- .add(serviceErrorGroupsComparisonStatisticsRoute)
- .add(serviceProfilingTimelineRoute)
- .add(serviceProfilingStatisticsRoute)
-
- // Agent configuration
- .add(getSingleAgentConfigurationRoute)
- .add(agentConfigurationAgentNameRoute)
- .add(agentConfigurationRoute)
- .add(agentConfigurationSearchRoute)
- .add(deleteAgentConfigurationRoute)
- .add(listAgentConfigurationEnvironmentsRoute)
- .add(listAgentConfigurationServicesRoute)
- .add(createOrUpdateAgentConfigurationRoute)
-
- // Correlations
- .add(correlationsLatencyDistributionRoute)
- .add(correlationsForSlowTransactionsRoute)
- .add(correlationsErrorDistributionRoute)
- .add(correlationsForFailedTransactionsRoute)
-
- // APM indices
- .add(apmIndexSettingsRoute)
- .add(apmIndicesRoute)
- .add(saveApmIndicesRoute)
-
- // Metrics
- .add(metricsChartsRoute)
- .add(serviceNodesRoute)
-
- // Traces
- .add(tracesRoute)
- .add(tracesByIdRoute)
- .add(rootTransactionByTraceIdRoute)
-
- // Transactions
- .add(transactionChartsBreakdownRoute)
- .add(transactionChartsDistributionRoute)
- .add(transactionChartsErrorRateRoute)
- .add(transactionGroupsRoute)
- .add(transactionGroupsPrimaryStatisticsRoute)
- .add(transactionLatencyChartsRoute)
- .add(transactionThroughputChartsRoute)
- .add(transactionGroupsComparisonStatisticsRoute)
-
- // Service map
- .add(serviceMapRoute)
- .add(serviceMapServiceNodeRoute)
-
- // Custom links
- .add(createCustomLinkRoute)
- .add(updateCustomLinkRoute)
- .add(deleteCustomLinkRoute)
- .add(listCustomLinksRoute)
- .add(customLinkTransactionRoute)
-
- // Observability dashboard
- .add(observabilityOverviewHasDataRoute)
- .add(observabilityOverviewRoute)
-
- // Anomaly detection
- .add(anomalyDetectionJobsRoute)
- .add(createAnomalyDetectionJobsRoute)
- .add(anomalyDetectionEnvironmentsRoute)
-
- // User Experience app api routes
- .add(rumOverviewLocalFiltersRoute)
- .add(rumPageViewsTrendRoute)
- .add(rumPageLoadDistributionRoute)
- .add(rumPageLoadDistBreakdownRoute)
- .add(rumClientMetricsRoute)
- .add(rumServicesRoute)
- .add(rumVisitorsBreakdownRoute)
- .add(rumWebCoreVitals)
- .add(rumJSErrors)
- .add(rumUrlSearch)
- .add(rumLongTaskMetrics)
- .add(rumHasDataRoute)
-
- // Alerting
- .add(transactionErrorCountChartPreview)
- .add(transactionDurationChartPreview)
- .add(transactionErrorRateChartPreview);
-
- return api;
-};
-
-export type APMAPI = ReturnType;
-
-export { createApmApi };
diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route.ts
new file mode 100644
index 0000000000000..86330a87a8c55
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/create_apm_server_route.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { createServerRouteFactory } from '@kbn/server-route-repository';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
+
+export const createApmServerRoute = createServerRouteFactory<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+>();
diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts
new file mode 100644
index 0000000000000..b7cbe890c57db
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { createServerRouteRepository } from '@kbn/server-route-repository';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
+
+export function createApmServerRouteRepository() {
+ return createServerRouteRepository<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+ >();
+}
diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts
deleted file mode 100644
index d74aac0992eb4..0000000000000
--- a/x-pack/plugins/apm/server/routes/create_route.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { CoreSetup } from 'src/core/server';
-import { HandlerReturn, Route, RouteParamsRT } from './typings';
-
-export function createRoute<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
->(
- route: Route
-): Route;
-
-export function createRoute<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
->(
- route: (core: CoreSetup) => Route
-): (core: CoreSetup) => Route;
-
-export function createRoute(routeOrFactoryFn: Function | object) {
- return routeOrFactoryFn;
-}
diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts
index 4aa7d7e6d412f..e06fbdf7fb6d4 100644
--- a/x-pack/plugins/apm/server/routes/environments.ts
+++ b/x-pack/plugins/apm/server/routes/environments.ts
@@ -9,10 +9,11 @@ import * as t from 'io-ts';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
import { getEnvironments } from '../lib/environments/get_environments';
-import { createRoute } from './create_route';
import { rangeRt } from './default_api_types';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const environmentsRoute = createRoute({
+const environmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/environments',
params: t.type({
query: t.intersection([
@@ -23,9 +24,10 @@ export const environmentsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -39,3 +41,7 @@ export const environmentsRoute = createRoute({
return { environments };
},
});
+
+export const environmentsRouteRepository = createApmServerRouteRepository().add(
+ environmentsRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts
index f69d3fc9631d1..d6bb1d4bcbaae 100644
--- a/x-pack/plugins/apm/server/routes/errors.ts
+++ b/x-pack/plugins/apm/server/routes/errors.ts
@@ -6,14 +6,15 @@
*/
import * as t from 'io-ts';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { getErrorDistribution } from '../lib/errors/distribution/get_distribution';
import { getErrorGroupSample } from '../lib/errors/get_error_group_sample';
import { getErrorGroups } from '../lib/errors/get_error_groups';
import { setupRequest } from '../lib/helpers/setup_request';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const errorsRoute = createRoute({
+const errorsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors',
params: t.type({
path: t.type({
@@ -30,9 +31,9 @@ export const errorsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const { serviceName } = params.path;
const { environment, kuery, sortField, sortDirection } = params.query;
@@ -49,7 +50,7 @@ export const errorsRoute = createRoute({
},
});
-export const errorGroupsRoute = createRoute({
+const errorGroupsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}',
params: t.type({
path: t.type({
@@ -59,10 +60,11 @@ export const errorGroupsRoute = createRoute({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName, groupId } = context.params.path;
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
+ const { serviceName, groupId } = params.path;
+ const { environment, kuery } = params.query;
return getErrorGroupSample({
environment,
@@ -74,7 +76,7 @@ export const errorGroupsRoute = createRoute({
},
});
-export const errorDistributionRoute = createRoute({
+const errorDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution',
params: t.type({
path: t.type({
@@ -90,9 +92,9 @@ export const errorDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const { serviceName } = params.path;
const { environment, kuery, groupId } = params.query;
return getErrorDistribution({
@@ -104,3 +106,8 @@ export const errorDistributionRoute = createRoute({
});
},
});
+
+export const errorsRouteRepository = createApmServerRouteRepository()
+ .add(errorsRoute)
+ .add(errorGroupsRoute)
+ .add(errorDistributionRoute);
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
new file mode 100644
index 0000000000000..c151752b4b6e0
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type {
+ ServerRouteRepository,
+ ReturnOf,
+ EndpointOf,
+} from '@kbn/server-route-repository';
+import { PickByValue } from 'utility-types';
+import { alertsChartPreviewRouteRepository } from './alerts/chart_preview';
+import { correlationsRouteRepository } from './correlations';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { environmentsRouteRepository } from './environments';
+import { errorsRouteRepository } from './errors';
+import { indexPatternRouteRepository } from './index_pattern';
+import { metricsRouteRepository } from './metrics';
+import { observabilityOverviewRouteRepository } from './observability_overview';
+import { rumRouteRepository } from './rum_client';
+import { serviceRouteRepository } from './services';
+import { serviceMapRouteRepository } from './service_map';
+import { serviceNodeRouteRepository } from './service_nodes';
+import { agentConfigurationRouteRepository } from './settings/agent_configuration';
+import { anomalyDetectionRouteRepository } from './settings/anomaly_detection';
+import { apmIndicesRouteRepository } from './settings/apm_indices';
+import { customLinkRouteRepository } from './settings/custom_link';
+import { traceRouteRepository } from './traces';
+import { transactionRouteRepository } from './transactions';
+import { APMRouteHandlerResources } from './typings';
+
+const getTypedGlobalApmServerRouteRepository = () => {
+ const repository = createApmServerRouteRepository()
+ .merge(indexPatternRouteRepository)
+ .merge(environmentsRouteRepository)
+ .merge(errorsRouteRepository)
+ .merge(metricsRouteRepository)
+ .merge(observabilityOverviewRouteRepository)
+ .merge(rumRouteRepository)
+ .merge(serviceMapRouteRepository)
+ .merge(serviceNodeRouteRepository)
+ .merge(serviceRouteRepository)
+ .merge(traceRouteRepository)
+ .merge(transactionRouteRepository)
+ .merge(alertsChartPreviewRouteRepository)
+ .merge(correlationsRouteRepository)
+ .merge(agentConfigurationRouteRepository)
+ .merge(anomalyDetectionRouteRepository)
+ .merge(apmIndicesRouteRepository)
+ .merge(customLinkRouteRepository);
+
+ return repository;
+};
+
+const getGlobalApmServerRouteRepository = () => {
+ return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository;
+};
+
+export type APMServerRouteRepository = ReturnType<
+ typeof getTypedGlobalApmServerRouteRepository
+>;
+
+// Ensure no APIs return arrays (or, by proxy, the any type),
+// to guarantee compatibility with _inspect.
+
+type CompositeEndpoint = EndpointOf;
+
+type EndpointReturnTypes = {
+ [Endpoint in CompositeEndpoint]: ReturnOf;
+};
+
+type ArrayLikeReturnTypes = PickByValue;
+
+type ViolatingEndpoints = keyof ArrayLikeReturnTypes;
+
+function assertType() {}
+
+// if any endpoint has an array-like return type, the assertion below will fail
+assertType();
+
+export { getGlobalApmServerRouteRepository };
diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts
index 3b800c23135ce..aa70cde4f96ae 100644
--- a/x-pack/plugins/apm/server/routes/index_pattern.ts
+++ b/x-pack/plugins/apm/server/routes/index_pattern.ts
@@ -6,49 +6,67 @@
*/
import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern';
-import { createRoute } from './create_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { setupRequest } from '../lib/helpers/setup_request';
-import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title';
import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern';
+import { createApmServerRoute } from './create_apm_server_route';
-export const staticIndexPatternRoute = createRoute((core) => ({
+const staticIndexPatternRoute = createApmServerRoute({
endpoint: 'POST /api/apm/index_pattern/static',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const {
+ request,
+ core,
+ plugins: { spaces },
+ config,
+ } = resources;
+
const [setup, savedObjectsClient] = await Promise.all([
- setupRequest(context, request),
- getInternalSavedObjectsClient(core),
+ setupRequest(resources),
+ core
+ .start()
+ .then((coreStart) => coreStart.savedObjects.createInternalRepository()),
]);
- const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request);
+ const spaceId = spaces?.setup.spacesService.getSpaceId(request);
const didCreateIndexPattern = await createStaticIndexPattern(
setup,
- context,
+ config,
savedObjectsClient,
spaceId
);
return { created: didCreateIndexPattern };
},
-}));
+});
-export const dynamicIndexPatternRoute = createRoute({
+const dynamicIndexPatternRoute = createApmServerRoute({
endpoint: 'GET /api/apm/index_pattern/dynamic',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
- const dynamicIndexPattern = await getDynamicIndexPattern({ context });
+ handler: async ({ context, config, logger }) => {
+ const dynamicIndexPattern = await getDynamicIndexPattern({
+ context,
+ config,
+ logger,
+ });
return { dynamicIndexPattern };
},
});
-export const apmIndexPatternTitleRoute = createRoute({
+const indexPatternTitleRoute = createApmServerRoute({
endpoint: 'GET /api/apm/index_pattern/title',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
+ handler: async ({ config }) => {
return {
- indexPatternTitle: getApmIndexPatternTitle(context),
+ indexPatternTitle: getApmIndexPatternTitle(config),
};
},
});
+
+export const indexPatternRouteRepository = createApmServerRouteRepository()
+ .add(staticIndexPatternRoute)
+ .add(dynamicIndexPatternRoute)
+ .add(indexPatternTitleRoute);
diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts
index c7e82e13d07b8..9fa2346eb72fb 100644
--- a/x-pack/plugins/apm/server/routes/metrics.ts
+++ b/x-pack/plugins/apm/server/routes/metrics.ts
@@ -8,10 +8,11 @@
import * as t from 'io-ts';
import { setupRequest } from '../lib/helpers/setup_request';
import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
-export const metricsChartsRoute = createRoute({
+const metricsChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts',
params: t.type({
path: t.type({
@@ -30,9 +31,9 @@ export const metricsChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const { serviceName } = params.path;
const { agentName, environment, kuery, serviceNodeName } = params.query;
return await getMetricsChartDataByAgent({
@@ -45,3 +46,7 @@ export const metricsChartsRoute = createRoute({
});
},
});
+
+export const metricsRouteRepository = createApmServerRouteRepository().add(
+ metricsChartsRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts
index 1aac2c09d01c5..d459570cf7337 100644
--- a/x-pack/plugins/apm/server/routes/observability_overview.ts
+++ b/x-pack/plugins/apm/server/routes/observability_overview.ts
@@ -10,30 +10,32 @@ import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceCount } from '../lib/observability_overview/get_service_count';
import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute';
import { getHasData } from '../lib/observability_overview/has_data';
-import { createRoute } from './create_route';
import { rangeRt } from './default_api_types';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { withApmSpan } from '../utils/with_apm_span';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { createApmServerRoute } from './create_apm_server_route';
-export const observabilityOverviewHasDataRoute = createRoute({
+const observabilityOverviewHasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview/has_data',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const res = await getHasData({ setup });
return { hasData: res };
},
});
-export const observabilityOverviewRoute = createRoute({
+const observabilityOverviewRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview',
params: t.type({
query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { bucketSize } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { bucketSize } = resources.params.query;
+
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -54,3 +56,7 @@ export const observabilityOverviewRoute = createRoute({
});
},
});
+
+export const observabilityOverviewRouteRepository = createApmServerRouteRepository()
+ .add(observabilityOverviewRoute)
+ .add(observabilityOverviewHasDataRoute);
diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts
new file mode 100644
index 0000000000000..82b73d46da5c1
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts
@@ -0,0 +1,507 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { jsonRt } from '@kbn/io-ts-utils';
+import { createServerRouteRepository } from '@kbn/server-route-repository';
+import { ServerRoute } from '@kbn/server-route-repository/target/typings';
+import * as t from 'io-ts';
+import { CoreSetup, Logger } from 'src/core/server';
+import { APMConfig } from '../..';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings';
+import { registerRoutes } from './index';
+
+type RegisterRouteDependencies = Parameters[0];
+
+const getRegisterRouteDependencies = () => {
+ const get = jest.fn();
+ const post = jest.fn();
+ const put = jest.fn();
+ const createRouter = jest.fn().mockReturnValue({
+ get,
+ post,
+ put,
+ });
+
+ const coreSetup = ({
+ http: {
+ createRouter,
+ },
+ } as unknown) as CoreSetup;
+
+ const logger = ({
+ error: jest.fn(),
+ } as unknown) as Logger;
+
+ return {
+ mocks: {
+ get,
+ post,
+ put,
+ createRouter,
+ coreSetup,
+ logger,
+ },
+ dependencies: ({
+ core: {
+ setup: coreSetup,
+ },
+ logger,
+ config: {} as APMConfig,
+ plugins: {},
+ } as unknown) as RegisterRouteDependencies,
+ };
+};
+
+const getRepository = () =>
+ createServerRouteRepository<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+ >();
+
+const initApi = (
+ routes: Array<
+ ServerRoute<
+ any,
+ t.Any,
+ APMRouteHandlerResources,
+ any,
+ APMRouteCreateOptions
+ >
+ >
+) => {
+ const { mocks, dependencies } = getRegisterRouteDependencies();
+
+ let repository = getRepository();
+
+ routes.forEach((route) => {
+ repository = repository.add(route);
+ });
+
+ registerRoutes({
+ ...dependencies,
+ repository,
+ });
+
+ const responseMock = {
+ ok: jest.fn(),
+ custom: jest.fn(),
+ };
+
+ const simulateRequest = (request: {
+ method: 'get' | 'post' | 'put';
+ pathname: string;
+ params?: Record;
+ body?: unknown;
+ query?: Record;
+ }) => {
+ const [, registeredRouteHandler] =
+ mocks[request.method].mock.calls.find((call) => {
+ return call[0].path === request.pathname;
+ }) ?? [];
+
+ const result = registeredRouteHandler(
+ {},
+ {
+ params: {},
+ query: {},
+ body: null,
+ ...request,
+ },
+ responseMock
+ );
+
+ return result;
+ };
+
+ return {
+ simulateRequest,
+ mocks: {
+ ...mocks,
+ response: responseMock,
+ },
+ };
+};
+
+describe('createApi', () => {
+ it('registers a route with the server', () => {
+ const {
+ mocks: { createRouter, get, post, put },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: { tags: ['access:apm'] },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'POST /bar',
+ params: t.type({
+ body: t.string,
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'PUT /baz',
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'GET /qux',
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ handler: async () => ({}),
+ },
+ ]);
+
+ expect(createRouter).toHaveBeenCalledTimes(1);
+
+ expect(get).toHaveBeenCalledTimes(2);
+ expect(post).toHaveBeenCalledTimes(1);
+ expect(put).toHaveBeenCalledTimes(1);
+
+ expect(get.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm'],
+ },
+ path: '/foo',
+ validate: expect.anything(),
+ });
+
+ expect(get.mock.calls[1][0]).toEqual({
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ path: '/qux',
+ validate: expect.anything(),
+ });
+
+ expect(post.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm'],
+ },
+ path: '/bar',
+ validate: expect.anything(),
+ });
+
+ expect(put.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ path: '/baz',
+ validate: expect.anything(),
+ });
+ });
+
+ describe('when validating', () => {
+ describe('_inspect', () => {
+ it('allows _inspect=true', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 'true' },
+ });
+
+ // responds with ok
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+ expect(params).toEqual({ query: { _inspect: true } });
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledWith({
+ body: { _inspect: [] },
+ });
+ });
+
+ it('rejects _inspect=1', async () => {
+ const handlerMock = jest.fn();
+
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ handler: handlerMock,
+ },
+ ]);
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 1 },
+ });
+
+ // responds with error handler
+ expect(response.ok).not.toHaveBeenCalled();
+ expect(response.custom).toHaveBeenCalledWith({
+ body: {
+ attributes: { _inspect: [] },
+ message:
+ 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
+ },
+ statusCode: 400,
+ });
+ });
+
+ it('allows omitting _inspect', async () => {
+ const handlerMock = jest.fn();
+
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock },
+ ]);
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {},
+ });
+
+ // responds with ok
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+ expect(params).toEqual({ query: { _inspect: false } });
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+
+ expect(response.ok).toHaveBeenCalledWith({ body: {} });
+ });
+ });
+
+ it('throws if unknown parameters are provided', async () => {
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 'true', extra: '' },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: { foo: 'bar' },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(2);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(3);
+ });
+
+ it('validates path parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: { tags: [] },
+ params: t.type({
+ path: t.type({
+ foo: t.string,
+ }),
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ },
+ });
+
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+
+ expect(response.ok).toHaveBeenCalledTimes(1);
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ path: {
+ foo: 'bar',
+ },
+ query: {
+ _inspect: false,
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ bar: 'foo',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 9,
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(2);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ extra: '',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(3);
+ });
+
+ it('validates body parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ params: t.type({
+ body: t.string,
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: '',
+ });
+
+ expect(response.custom).not.toHaveBeenCalled();
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledTimes(1);
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ body: '',
+ query: {
+ _inspect: false,
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: null,
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+ });
+
+ it('validates query parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ params: t.type({
+ query: t.type({
+ bar: t.string,
+ filterNames: jsonRt.pipe(t.array(t.string)),
+ }),
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {
+ bar: '',
+ _inspect: 'true',
+ filterNames: JSON.stringify(['hostName', 'agentName']),
+ },
+ });
+
+ expect(response.custom).not.toHaveBeenCalled();
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledTimes(1);
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ query: {
+ bar: '',
+ _inspect: true,
+ filterNames: ['hostName', 'agentName'],
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {
+ bar: '',
+ foo: '',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts
new file mode 100644
index 0000000000000..3a88a496b923f
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts
@@ -0,0 +1,143 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Boom from '@hapi/boom';
+import * as t from 'io-ts';
+import { KibanaRequest, RouteRegistrar } from 'src/core/server';
+import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
+import agent from 'elastic-apm-node';
+import { ServerRouteRepository } from '@kbn/server-route-repository';
+import { merge } from 'lodash';
+import {
+ decodeRequestParams,
+ parseEndpoint,
+ routeValidationObject,
+} from '@kbn/server-route-repository';
+import { mergeRt, jsonRt } from '@kbn/io-ts-utils';
+import { pickKeys } from '../../../common/utils/pick_keys';
+import { APMRouteHandlerResources, InspectResponse } from '../typings';
+import type { ApmPluginRequestHandlerContext } from '../typings';
+
+const inspectRt = t.exact(
+ t.partial({
+ query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
+ })
+);
+
+export const inspectableEsQueriesMap = new WeakMap<
+ KibanaRequest,
+ InspectResponse
+>();
+
+export function registerRoutes({
+ core,
+ repository,
+ plugins,
+ logger,
+ config,
+}: {
+ core: APMRouteHandlerResources['core'];
+ plugins: APMRouteHandlerResources['plugins'];
+ logger: APMRouteHandlerResources['logger'];
+ repository: ServerRouteRepository;
+ config: APMRouteHandlerResources['config'];
+}) {
+ const routes = repository.getRoutes();
+
+ const router = core.setup.http.createRouter();
+
+ routes.forEach((route) => {
+ const { params, endpoint, options, handler } = route;
+
+ const { method, pathname } = parseEndpoint(endpoint);
+
+ (router[method] as RouteRegistrar<
+ typeof method,
+ ApmPluginRequestHandlerContext
+ >)(
+ {
+ path: pathname,
+ options,
+ validate: routeValidationObject,
+ },
+ async (context, request, response) => {
+ if (agent.isStarted()) {
+ agent.addLabels({
+ plugin: 'apm',
+ });
+ }
+
+ // init debug queries
+ inspectableEsQueriesMap.set(request, []);
+
+ try {
+ const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt;
+
+ const validatedParams = decodeRequestParams(
+ pickKeys(request, 'params', 'body', 'query'),
+ runtimeType
+ );
+
+ const data: Record | undefined | null = (await handler({
+ request,
+ context,
+ config,
+ logger,
+ core,
+ plugins,
+ params: merge(
+ {
+ query: {
+ _inspect: false,
+ },
+ },
+ validatedParams
+ ),
+ })) as any;
+
+ if (Array.isArray(data)) {
+ throw new Error('Return type cannot be an array');
+ }
+
+ const body = validatedParams.query?._inspect
+ ? {
+ ...data,
+ _inspect: inspectableEsQueriesMap.get(request),
+ }
+ : { ...data };
+
+ // cleanup
+ inspectableEsQueriesMap.delete(request);
+
+ return response.ok({ body });
+ } catch (error) {
+ logger.error(error);
+ const opts = {
+ statusCode: 500,
+ body: {
+ message: error.message,
+ attributes: {
+ _inspect: inspectableEsQueriesMap.get(request),
+ },
+ },
+ };
+
+ if (Boom.isBoom(error)) {
+ opts.statusCode = error.output.statusCode;
+ }
+
+ if (error instanceof RequestAbortedError) {
+ opts.statusCode = 499;
+ opts.body.message = 'Client closed request';
+ }
+
+ return response.custom(opts);
+ }
+ }
+ );
+ });
+}
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
index 3156acb469a72..d7f91adc0d683 100644
--- a/x-pack/plugins/apm/server/routes/rum_client.ts
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
-import { jsonRt } from '../../common/runtime_types/json_rt';
+import { jsonRt } from '@kbn/io-ts-utils';
import { LocalUIFilterName } from '../../common/ui_filter';
import {
Setup,
@@ -28,9 +28,10 @@ import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters
import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config';
import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions';
import { Projection } from '../projections/typings';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { rangeRt } from './default_api_types';
-import { APMRequestHandlerContext } from './typings';
+import { APMRouteHandlerResources } from './typings';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@@ -45,18 +46,18 @@ const uxQueryRt = t.intersection([
t.partial({ urlQuery: t.string, percentile: t.string }),
]);
-export const rumClientMetricsRoute = createRoute({
+const rumClientMetricsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum/client-metrics',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getClientMetrics({
setup,
@@ -66,18 +67,18 @@ export const rumClientMetricsRoute = createRoute({
},
});
-export const rumPageLoadDistributionRoute = createRoute({
+const rumPageLoadDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-load-distribution',
params: t.type({
query: t.intersection([uxQueryRt, percentileRangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { minPercentile, maxPercentile, urlQuery },
- } = context.params;
+ } = resources.params;
const pageLoadDistribution = await getPageLoadDistribution({
setup,
@@ -90,7 +91,7 @@ export const rumPageLoadDistributionRoute = createRoute({
},
});
-export const rumPageLoadDistBreakdownRoute = createRoute({
+const rumPageLoadDistBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown',
params: t.type({
query: t.intersection([
@@ -100,12 +101,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { minPercentile, maxPercentile, breakdown, urlQuery },
- } = context.params;
+ } = resources.params;
const pageLoadDistBreakdown = await getPageLoadDistBreakdown({
setup,
@@ -119,18 +120,18 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
},
});
-export const rumPageViewsTrendRoute = createRoute({
+const rumPageViewsTrendRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-view-trends',
params: t.type({
query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { breakdowns, urlQuery },
- } = context.params;
+ } = resources.params;
return getPageViewTrends({
setup,
@@ -140,32 +141,32 @@ export const rumPageViewsTrendRoute = createRoute({
},
});
-export const rumServicesRoute = createRoute({
+const rumServicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/services',
params: t.type({
query: t.intersection([uiFiltersRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const rumServices = await getRumServices({ setup });
return { rumServices };
},
});
-export const rumVisitorsBreakdownRoute = createRoute({
+const rumVisitorsBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/visitor-breakdown',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery },
- } = context.params;
+ } = resources.params;
return getVisitorBreakdown({
setup,
@@ -174,18 +175,18 @@ export const rumVisitorsBreakdownRoute = createRoute({
},
});
-export const rumWebCoreVitals = createRoute({
+const rumWebCoreVitals = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/web-core-vitals',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getWebCoreVitals({
setup,
@@ -195,18 +196,18 @@ export const rumWebCoreVitals = createRoute({
},
});
-export const rumLongTaskMetrics = createRoute({
+const rumLongTaskMetrics = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/long-task-metrics',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getLongTaskMetrics({
setup,
@@ -216,24 +217,24 @@ export const rumLongTaskMetrics = createRoute({
},
});
-export const rumUrlSearch = createRoute({
+const rumUrlSearch = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/url-search',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) });
},
});
-export const rumJSErrors = createRoute({
+const rumJSErrors = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/js-errors',
params: t.type({
query: t.intersection([
@@ -244,12 +245,12 @@ export const rumJSErrors = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { pageSize, pageIndex, urlQuery },
- } = context.params;
+ } = resources.params;
return getJSErrors({
setup,
@@ -260,14 +261,14 @@ export const rumJSErrors = createRoute({
},
});
-export const rumHasDataRoute = createRoute({
+const rumHasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
params: t.type({
query: t.intersection([uiFiltersRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
return await hasRumData({ setup });
},
});
@@ -309,21 +310,22 @@ function createLocalFiltersRoute<
>;
queryRt: TQueryRT;
}) {
- return createRoute({
+ return createApmServerRoute({
endpoint,
params: t.type({
query: t.intersection([localUiBaseQueryRt, queryRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const { uiFilters } = setup;
- const { query } = context.params;
+
+ const { query } = resources.params;
const { filterNames } = query;
const projection = await getProjection({
query,
- context,
+ resources,
setup,
});
@@ -339,7 +341,7 @@ function createLocalFiltersRoute<
});
}
-export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
+const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
endpoint: 'GET /api/apm/rum/local_filters',
getProjection: async ({ setup }) => {
return getRumPageLoadTransactionsProjection({
@@ -357,9 +359,23 @@ type GetProjection<
> = ({
query,
setup,
- context,
+ resources,
}: {
query: t.TypeOf;
setup: Setup & SetupTimeRange;
- context: APMRequestHandlerContext;
+ resources: APMRouteHandlerResources;
}) => Promise | TProjection;
+
+export const rumRouteRepository = createApmServerRouteRepository()
+ .add(rumClientMetricsRoute)
+ .add(rumPageLoadDistributionRoute)
+ .add(rumPageLoadDistBreakdownRoute)
+ .add(rumPageViewsTrendRoute)
+ .add(rumServicesRoute)
+ .add(rumVisitorsBreakdownRoute)
+ .add(rumWebCoreVitals)
+ .add(rumLongTaskMetrics)
+ .add(rumUrlSearch)
+ .add(rumJSErrors)
+ .add(rumHasDataRoute)
+ .add(rumOverviewLocalFiltersRoute);
diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index 33943d6e05d01..267479de4c102 100644
--- a/x-pack/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/plugins/apm/server/routes/service_map.ts
@@ -11,13 +11,14 @@ import { invalidLicenseMessage } from '../../common/service_map';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceMap } from '../lib/service_map/get_service_map';
import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { environmentRt, rangeRt } from './default_api_types';
import { notifyFeatureUsage } from '../feature';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { isActivePlatinumLicense } from '../../common/license_check';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const serviceMapRoute = createRoute({
+const serviceMapRoute = createApmServerRoute({
endpoint: 'GET /api/apm/service-map',
params: t.type({
query: t.intersection([
@@ -29,8 +30,9 @@ export const serviceMapRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- if (!context.config['xpack.apm.serviceMapEnabled']) {
+ handler: async (resources) => {
+ const { config, context, params, logger } = resources;
+ if (!config['xpack.apm.serviceMapEnabled']) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
@@ -42,11 +44,10 @@ export const serviceMapRoute = createRoute({
featureName: 'serviceMaps',
});
- const logger = context.logger;
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
query: { serviceName, environment },
- } = context.params;
+ } = params;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -61,7 +62,7 @@ export const serviceMapRoute = createRoute({
},
});
-export const serviceMapServiceNodeRoute = createRoute({
+const serviceMapServiceNodeRoute = createApmServerRoute({
endpoint: 'GET /api/apm/service-map/service/{serviceName}',
params: t.type({
path: t.type({
@@ -70,19 +71,21 @@ export const serviceMapServiceNodeRoute = createRoute({
query: t.intersection([environmentRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- if (!context.config['xpack.apm.serviceMapEnabled']) {
+ handler: async (resources) => {
+ const { config, context, params } = resources;
+
+ if (!config['xpack.apm.serviceMapEnabled']) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(invalidLicenseMessage);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
path: { serviceName },
query: { environment },
- } = context.params;
+ } = params;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -96,3 +99,7 @@ export const serviceMapServiceNodeRoute = createRoute({
});
},
});
+
+export const serviceMapRouteRepository = createApmServerRouteRepository()
+ .add(serviceMapRoute)
+ .add(serviceMapServiceNodeRoute);
diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts
index e9060688c63a6..a2eb12662cbca 100644
--- a/x-pack/plugins/apm/server/routes/service_nodes.ts
+++ b/x-pack/plugins/apm/server/routes/service_nodes.ts
@@ -6,12 +6,13 @@
*/
import * as t from 'io-ts';
-import { createRoute } from './create_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { createApmServerRoute } from './create_apm_server_route';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceNodes } from '../lib/service_nodes';
import { rangeRt, kueryRt } from './default_api_types';
-export const serviceNodesRoute = createRoute({
+const serviceNodesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes',
params: t.type({
path: t.type({
@@ -20,9 +21,9 @@ export const serviceNodesRoute = createRoute({
query: t.intersection([kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const { serviceName } = params.path;
const { kuery } = params.query;
@@ -30,3 +31,7 @@ export const serviceNodesRoute = createRoute({
return { serviceNodes };
},
});
+
+export const serviceNodeRouteRepository = createApmServerRouteRepository().add(
+ serviceNodesRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts
index b4d25ca8b2a06..800a5bdcc5d5f 100644
--- a/x-pack/plugins/apm/server/routes/services.ts
+++ b/x-pack/plugins/apm/server/routes/services.ts
@@ -6,15 +6,12 @@
*/
import Boom from '@hapi/boom';
+import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { uniq } from 'lodash';
-import {
- LatencyAggregationType,
- latencyAggregationTypeRt,
-} from '../../common/latency_aggregation_types';
+import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types';
import { ProfilingValueType } from '../../common/profiling';
import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt';
-import { jsonRt } from '../../common/runtime_types/json_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
@@ -35,7 +32,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser
import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline';
import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate';
import { withApmSpan } from '../utils/with_apm_span';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import {
comparisonRangeRt,
environmentRt,
@@ -43,15 +41,16 @@ import {
rangeRt,
} from './default_api_types';
-export const servicesRoute = createRoute({
+const servicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services',
params: t.type({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+ const { environment, kuery } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -61,21 +60,22 @@ export const servicesRoute = createRoute({
kuery,
setup,
searchAggregatedTransactions,
- logger: context.logger,
+ logger,
});
},
});
-export const serviceMetadataDetailsRoute = createRoute({
+const serviceMetadataDetailsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metadata/details',
params: t.type({
path: t.type({ serviceName: t.string }),
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -89,16 +89,17 @@ export const serviceMetadataDetailsRoute = createRoute({
},
});
-export const serviceMetadataIconsRoute = createRoute({
+const serviceMetadataIconsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons',
params: t.type({
path: t.type({ serviceName: t.string }),
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -112,7 +113,7 @@ export const serviceMetadataIconsRoute = createRoute({
},
});
-export const serviceAgentNameRoute = createRoute({
+const serviceAgentNameRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/agent_name',
params: t.type({
path: t.type({
@@ -121,9 +122,10 @@ export const serviceAgentNameRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -136,7 +138,7 @@ export const serviceAgentNameRoute = createRoute({
},
});
-export const serviceTransactionTypesRoute = createRoute({
+const serviceTransactionTypesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transaction_types',
params: t.type({
path: t.type({
@@ -145,9 +147,11 @@ export const serviceTransactionTypesRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+
return getServiceTransactionTypes({
serviceName,
setup,
@@ -158,7 +162,7 @@ export const serviceTransactionTypesRoute = createRoute({
},
});
-export const serviceNodeMetadataRoute = createRoute({
+const serviceNodeMetadataRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
params: t.type({
@@ -169,10 +173,11 @@ export const serviceNodeMetadataRoute = createRoute({
query: t.intersection([kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName, serviceNodeName } = context.params.path;
- const { kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName, serviceNodeName } = params.path;
+ const { kuery } = params.query;
return getServiceNodeMetadata({
kuery,
@@ -183,7 +188,7 @@ export const serviceNodeMetadataRoute = createRoute({
},
});
-export const serviceAnnotationsRoute = createRoute({
+const serviceAnnotationsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: t.type({
path: t.type({
@@ -192,12 +197,13 @@ export const serviceAnnotationsRoute = createRoute({
query: t.intersection([environmentRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, plugins, context, request, logger } = resources;
+ const { serviceName } = params.path;
+ const { environment } = params.query;
- const { observability } = context.plugins;
+ const { observability } = plugins;
const [
annotationsClient,
@@ -205,7 +211,7 @@ export const serviceAnnotationsRoute = createRoute({
] = await Promise.all([
observability
? withApmSpan('get_scoped_annotations_client', () =>
- observability.getScopedAnnotationsClient(context, request)
+ observability.setup.getScopedAnnotationsClient(context, request)
)
: undefined,
getSearchAggregatedTransactions(setup),
@@ -218,12 +224,12 @@ export const serviceAnnotationsRoute = createRoute({
serviceName,
annotationsClient,
client: context.core.elasticsearch.client.asCurrentUser,
- logger: context.logger,
+ logger,
});
},
});
-export const serviceAnnotationsCreateRoute = createRoute({
+const serviceAnnotationsCreateRoute = createApmServerRoute({
endpoint: 'POST /api/apm/services/{serviceName}/annotation',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -250,12 +256,17 @@ export const serviceAnnotationsCreateRoute = createRoute({
}),
]),
}),
- handler: async ({ request, context }) => {
- const { observability } = context.plugins;
+ handler: async (resources) => {
+ const {
+ request,
+ context,
+ plugins: { observability },
+ params,
+ } = resources;
const annotationsClient = observability
? await withApmSpan('get_scoped_annotations_client', () =>
- observability.getScopedAnnotationsClient(context, request)
+ observability.setup.getScopedAnnotationsClient(context, request)
)
: undefined;
@@ -263,7 +274,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
throw Boom.notFound();
}
- const { body, path } = context.params;
+ const { body, path } = params;
return withApmSpan('create_annotation', () =>
annotationsClient.create({
@@ -283,7 +294,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
},
});
-export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
+const serviceErrorGroupsPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics',
params: t.type({
@@ -300,13 +311,14 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
query: { kuery, transactionType, environment },
- } = context.params;
+ } = params;
return getServiceErrorGroupPrimaryStatistics({
kuery,
serviceName,
@@ -317,7 +329,7 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
},
});
-export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
+const serviceErrorGroupsComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics',
params: t.type({
@@ -337,8 +349,9 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
@@ -351,7 +364,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
comparisonStart,
comparisonEnd,
},
- } = context.params;
+ } = params;
return getServiceErrorGroupPeriods({
environment,
@@ -367,7 +380,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
},
});
-export const serviceThroughputRoute = createRoute({
+const serviceThroughputRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/throughput',
params: t.type({
path: t.type({
@@ -382,16 +395,17 @@ export const serviceThroughputRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionType,
comparisonStart,
comparisonEnd,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -432,7 +446,7 @@ export const serviceThroughputRoute = createRoute({
},
});
-export const serviceInstancesPrimaryStatisticsRoute = createRoute({
+const serviceInstancesPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics',
params: t.type({
@@ -450,12 +464,16 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment, kuery, transactionType } = context.params.query;
- const latencyAggregationType = (context.params.query
- .latencyAggregationType as unknown) as LatencyAggregationType;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const {
+ environment,
+ kuery,
+ transactionType,
+ latencyAggregationType,
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -479,7 +497,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
},
});
-export const serviceInstancesComparisonStatisticsRoute = createRoute({
+const serviceInstancesComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics',
params: t.type({
@@ -500,9 +518,10 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -511,9 +530,8 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
comparisonEnd,
serviceNodeIds,
numBuckets,
- } = context.params.query;
- const latencyAggregationType = (context.params.query
- .latencyAggregationType as unknown) as LatencyAggregationType;
+ latencyAggregationType,
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -535,7 +553,7 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
},
});
-export const serviceDependenciesRoute = createRoute({
+const serviceDependenciesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
params: t.type({
path: t.type({
@@ -552,11 +570,11 @@ export const serviceDependenciesRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
-
- const { serviceName } = context.params.path;
- const { environment, numBuckets } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const { environment, numBuckets } = params.query;
const serviceDependencies = await getServiceDependencies({
serviceName,
@@ -569,7 +587,7 @@ export const serviceDependenciesRoute = createRoute({
},
});
-export const serviceProfilingTimelineRoute = createRoute({
+const serviceProfilingTimelineRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline',
params: t.type({
path: t.type({
@@ -580,13 +598,13 @@ export const serviceProfilingTimelineRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
-
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
query: { environment, kuery },
- } = context.params;
+ } = params;
const profilingTimeline = await getServiceProfilingTimeline({
kuery,
@@ -599,7 +617,7 @@ export const serviceProfilingTimelineRoute = createRoute({
},
});
-export const serviceProfilingStatisticsRoute = createRoute({
+const serviceProfilingStatisticsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics',
params: t.type({
path: t.type({
@@ -625,13 +643,15 @@ export const serviceProfilingStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params, logger } = resources;
const {
path: { serviceName },
query: { environment, kuery, valueType },
- } = context.params;
+ } = params;
return getServiceProfilingStatistics({
kuery,
@@ -639,7 +659,25 @@ export const serviceProfilingStatisticsRoute = createRoute({
environment,
valueType,
setup,
- logger: context.logger,
+ logger,
});
},
});
+
+export const serviceRouteRepository = createApmServerRouteRepository()
+ .add(servicesRoute)
+ .add(serviceMetadataDetailsRoute)
+ .add(serviceMetadataIconsRoute)
+ .add(serviceAgentNameRoute)
+ .add(serviceTransactionTypesRoute)
+ .add(serviceNodeMetadataRoute)
+ .add(serviceAnnotationsRoute)
+ .add(serviceAnnotationsCreateRoute)
+ .add(serviceErrorGroupsPrimaryStatisticsRoute)
+ .add(serviceErrorGroupsComparisonStatisticsRoute)
+ .add(serviceThroughputRoute)
+ .add(serviceInstancesPrimaryStatisticsRoute)
+ .add(serviceInstancesComparisonStatisticsRoute)
+ .add(serviceDependenciesRoute)
+ .add(serviceProfilingTimelineRoute)
+ .add(serviceProfilingStatisticsRoute);
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 31e8d6cc1e9f0..111e0a18c8608 100644
--- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
+++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
@@ -16,7 +16,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f
import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations';
import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments';
import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service';
import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent';
import {
@@ -24,34 +24,37 @@ import {
agentConfigurationIntakeRt,
} from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
// get list of configurations
-export const agentConfigurationRoute = createRoute({
+const agentConfigurationRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const configurations = await listConfigurations({ setup });
return { configurations };
},
});
// get a single configuration
-export const getSingleAgentConfigurationRoute = createRoute({
+const getSingleAgentConfigurationRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/view',
params: t.partial({
query: serviceRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { name, environment } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { name, environment } = params.query;
const service = { name, environment };
const config = await findExactConfiguration({ service, setup });
if (!config) {
- context.logger.info(
+ logger.info(
`Config was not found for ${service.name}/${service.environment}`
);
@@ -63,7 +66,7 @@ export const getSingleAgentConfigurationRoute = createRoute({
});
// delete configuration
-export const deleteAgentConfigurationRoute = createRoute({
+const deleteAgentConfigurationRoute = createApmServerRoute({
endpoint: 'DELETE /api/apm/settings/agent-configuration',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -73,20 +76,22 @@ export const deleteAgentConfigurationRoute = createRoute({
service: serviceRt,
}),
}),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { service } = context.params.body;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { service } = params.body;
const config = await findExactConfiguration({ service, setup });
if (!config) {
- context.logger.info(
+ logger.info(
`Config was not found for ${service.name}/${service.environment}`
);
throw Boom.notFound();
}
- context.logger.info(
+ logger.info(
`Deleting config ${service.name}/${service.environment} (${config._id})`
);
@@ -98,7 +103,7 @@ export const deleteAgentConfigurationRoute = createRoute({
});
// create/update configuration
-export const createOrUpdateAgentConfigurationRoute = createRoute({
+const createOrUpdateAgentConfigurationRoute = createApmServerRoute({
endpoint: 'PUT /api/apm/settings/agent-configuration',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -107,9 +112,10 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
t.partial({ query: t.partial({ overwrite: toBooleanRt }) }),
t.type({ body: agentConfigurationIntakeRt }),
]),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { body, query } = context.params;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+ const { body, query } = params;
// if the config already exists, it is fetched and updated
// this is to avoid creating two configs with identical service params
@@ -125,13 +131,13 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
);
}
- context.logger.info(
+ logger.info(
`${config ? 'Updating' : 'Creating'} config ${body.service.name}/${
body.service.environment
}`
);
- return await createOrUpdateConfiguration({
+ await createOrUpdateConfiguration({
configurationId: config?._id,
configurationIntake: body,
setup,
@@ -147,35 +153,35 @@ const searchParamsRt = t.intersection([
export type AgentConfigSearchParams = t.TypeOf;
// Lookup single configuration (used by APM Server)
-export const agentConfigurationSearchRoute = createRoute({
+const agentConfigurationSearchRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/agent-configuration/search',
params: t.type({
body: searchParamsRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, logger } = resources;
+
const {
service,
etag,
mark_as_applied_by_agent: markAsAppliedByAgent,
- } = context.params.body;
+ } = params.body;
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const config = await searchConfigurations({
service,
setup,
});
if (!config) {
- context.logger.debug(
+ logger.debug(
`[Central configuration] Config was not found for ${service.name}/${service.environment}`
);
throw Boom.notFound();
}
- context.logger.info(
- `Config was found for ${service.name}/${service.environment}`
- );
+ logger.info(`Config was found for ${service.name}/${service.environment}`);
// update `applied_by_agent` field
// when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags)
@@ -197,11 +203,11 @@ export const agentConfigurationSearchRoute = createRoute({
*/
// get list of services
-export const listAgentConfigurationServicesRoute = createRoute({
+const listAgentConfigurationServicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/services',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -215,15 +221,17 @@ export const listAgentConfigurationServicesRoute = createRoute({
});
// get environments for service
-export const listAgentConfigurationEnvironmentsRoute = createRoute({
+const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/environments',
params: t.partial({
query: t.partial({ serviceName: t.string }),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -239,16 +247,27 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({
});
// get agentName for service
-export const agentConfigurationAgentNameRoute = createRoute({
+const agentConfigurationAgentNameRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/agent_name',
params: t.type({
query: t.type({ serviceName: t.string }),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.query;
const agentName = await getAgentNameByService({ serviceName, setup });
return { agentName };
},
});
+
+export const agentConfigurationRouteRepository = createApmServerRouteRepository()
+ .add(agentConfigurationRoute)
+ .add(getSingleAgentConfigurationRoute)
+ .add(deleteAgentConfigurationRoute)
+ .add(createOrUpdateAgentConfigurationRoute)
+ .add(agentConfigurationSearchRoute)
+ .add(listAgentConfigurationServicesRoute)
+ .add(listAgentConfigurationEnvironmentsRoute)
+ .add(agentConfigurationAgentNameRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index de7f35c4081bc..98467e1a4a0dd 100644
--- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
+++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
@@ -9,7 +9,7 @@ import * as t from 'io-ts';
import Boom from '@hapi/boom';
import { isActivePlatinumLicense } from '../../../common/license_check';
import { ML_ERRORS } from '../../../common/anomaly_detection';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs';
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
import { setupRequest } from '../../lib/helpers/setup_request';
@@ -18,15 +18,17 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
import { notifyFeatureUsage } from '../../feature';
import { withApmSpan } from '../../utils/with_apm_span';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
// get ML anomaly detection jobs for each environment
-export const anomalyDetectionJobsRoute = createRoute({
+const anomalyDetectionJobsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/anomaly-detection/jobs',
options: {
tags: ['access:apm', 'access:ml:canGetJobs'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { context, logger } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
@@ -34,7 +36,7 @@ export const anomalyDetectionJobsRoute = createRoute({
const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () =>
Promise.all([
- getAnomalyDetectionJobs(setup, context.logger),
+ getAnomalyDetectionJobs(setup, logger),
hasLegacyJobs(setup),
])
);
@@ -47,7 +49,7 @@ export const anomalyDetectionJobsRoute = createRoute({
});
// create new ML anomaly detection jobs for each given environment
-export const createAnomalyDetectionJobsRoute = createRoute({
+const createAnomalyDetectionJobsRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/anomaly-detection/jobs',
options: {
tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'],
@@ -57,15 +59,17 @@ export const createAnomalyDetectionJobsRoute = createRoute({
environments: t.array(t.string),
}),
}),
- handler: async ({ context, request }) => {
- const { environments } = context.params.body;
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params, context, logger } = resources;
+ const { environments } = params.body;
+
+ const setup = await setupRequest(resources);
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
}
- await createAnomalyDetectionJobs(setup, environments, context.logger);
+ await createAnomalyDetectionJobs(setup, environments, logger);
notifyFeatureUsage({
licensingPlugin: context.licensing,
@@ -77,11 +81,11 @@ export const createAnomalyDetectionJobsRoute = createRoute({
});
// get all available environments to create anomaly detection jobs for
-export const anomalyDetectionEnvironmentsRoute = createRoute({
+const anomalyDetectionEnvironmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/anomaly-detection/environments',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -96,3 +100,8 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({
return { environments };
},
});
+
+export const anomalyDetectionRouteRepository = createApmServerRouteRepository()
+ .add(anomalyDetectionJobsRoute)
+ .add(createAnomalyDetectionJobsRoute)
+ .add(anomalyDetectionEnvironmentsRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
index 91057c97579e4..003471aa89f39 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
@@ -6,7 +6,8 @@
*/
import * as t from 'io-ts';
-import { createRoute } from '../create_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
+import { createApmServerRoute } from '../create_apm_server_route';
import {
getApmIndices,
getApmIndexSettings,
@@ -14,29 +15,30 @@ import {
import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices';
// get list of apm indices and values
-export const apmIndexSettingsRoute = createRoute({
+const apmIndexSettingsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/apm-index-settings',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
- const apmIndexSettings = await getApmIndexSettings({ context });
+ handler: async ({ config, context }) => {
+ const apmIndexSettings = await getApmIndexSettings({ config, context });
return { apmIndexSettings };
},
});
// get apm indices configuration object
-export const apmIndicesRoute = createRoute({
+const apmIndicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/apm-indices',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
+ handler: async (resources) => {
+ const { context, config } = resources;
return await getApmIndices({
savedObjectsClient: context.core.savedObjects.client,
- config: context.config,
+ config,
});
},
});
// save ui indices
-export const saveApmIndicesRoute = createRoute({
+const saveApmIndicesRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/apm-indices/save',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -53,9 +55,15 @@ export const saveApmIndicesRoute = createRoute({
/* eslint-enable @typescript-eslint/naming-convention */
}),
}),
- handler: async ({ context }) => {
- const { body } = context.params;
+ handler: async (resources) => {
+ const { params, context } = resources;
+ const { body } = params;
const savedObjectsClient = context.core.savedObjects.client;
return await saveApmIndices(savedObjectsClient, body);
},
});
+
+export const apmIndicesRouteRepository = createApmServerRouteRepository()
+ .add(apmIndexSettingsRoute)
+ .add(apmIndicesRoute)
+ .add(saveApmIndicesRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
index a6ab553f09419..c9c5d236c14f9 100644
--- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts
+++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
@@ -21,35 +21,40 @@ import {
import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link';
import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
-export const customLinkTransactionRoute = createRoute({
+const customLinkTransactionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/custom_links/transaction',
options: { tags: ['access:apm'] },
params: t.partial({
query: filterOptionsRt,
}),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { query } = context.params;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { query } = params;
// picks only the items listed in FILTER_OPTIONS
const filters = pick(query, FILTER_OPTIONS);
return await getTransaction({ setup, filters });
},
});
-export const listCustomLinksRoute = createRoute({
+const listCustomLinksRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/custom_links',
options: { tags: ['access:apm'] },
params: t.partial({
query: filterOptionsRt,
}),
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { query } = context.params;
+ const setup = await setupRequest(resources);
+
+ const { query } = params;
+
// picks only the items listed in FILTER_OPTIONS
const filters = pick(query, FILTER_OPTIONS);
const customLinks = await listCustomLinks({ setup, filters });
@@ -57,29 +62,30 @@ export const listCustomLinksRoute = createRoute({
},
});
-export const createCustomLinkRoute = createRoute({
+const createCustomLinkRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/custom_links',
params: t.type({
body: payloadRt,
}),
options: { tags: ['access:apm', 'access:apm_write'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const customLink = context.params.body;
- const res = await createOrUpdateCustomLink({ customLink, setup });
+ const setup = await setupRequest(resources);
+ const customLink = params.body;
notifyFeatureUsage({
licensingPlugin: context.licensing,
featureName: 'customLinks',
});
- return res;
+
+ await createOrUpdateCustomLink({ customLink, setup });
},
});
-export const updateCustomLinkRoute = createRoute({
+const updateCustomLinkRoute = createApmServerRoute({
endpoint: 'PUT /api/apm/settings/custom_links/{id}',
params: t.type({
path: t.type({
@@ -90,23 +96,26 @@ export const updateCustomLinkRoute = createRoute({
options: {
tags: ['access:apm', 'access:apm_write'],
},
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, context } = resources;
+
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { id } = context.params.path;
- const customLink = context.params.body;
- const res = await createOrUpdateCustomLink({
+ const setup = await setupRequest(resources);
+
+ const { id } = params.path;
+ const customLink = params.body;
+
+ await createOrUpdateCustomLink({
customLinkId: id,
customLink,
setup,
});
- return res;
},
});
-export const deleteCustomLinkRoute = createRoute({
+const deleteCustomLinkRoute = createApmServerRoute({
endpoint: 'DELETE /api/apm/settings/custom_links/{id}',
params: t.type({
path: t.type({
@@ -116,12 +125,14 @@ export const deleteCustomLinkRoute = createRoute({
options: {
tags: ['access:apm', 'access:apm_write'],
},
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
+
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { id } = context.params.path;
+ const setup = await setupRequest(resources);
+ const { id } = params.path;
const res = await deleteCustomLink({
customLinkId: id,
setup,
@@ -129,3 +140,10 @@ export const deleteCustomLinkRoute = createRoute({
return res;
},
});
+
+export const customLinkRouteRepository = createApmServerRouteRepository()
+ .add(customLinkTransactionRoute)
+ .add(listCustomLinksRoute)
+ .add(createCustomLinkRoute)
+ .add(updateCustomLinkRoute)
+ .add(deleteCustomLinkRoute);
diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts
index 6287ffbf0c751..dd392982b02fd 100644
--- a/x-pack/plugins/apm/server/routes/traces.ts
+++ b/x-pack/plugins/apm/server/routes/traces.ts
@@ -9,20 +9,22 @@ import * as t from 'io-ts';
import { setupRequest } from '../lib/helpers/setup_request';
import { getTrace } from '../lib/traces/get_trace';
import { getTransactionGroupList } from '../lib/transaction_groups';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const tracesRoute = createRoute({
+const tracesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces',
params: t.type({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { environment, kuery } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -34,7 +36,7 @@ export const tracesRoute = createRoute({
},
});
-export const tracesByIdRoute = createRoute({
+const tracesByIdRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces/{traceId}',
params: t.type({
path: t.type({
@@ -43,13 +45,16 @@ export const tracesByIdRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- return getTrace(context.params.path.traceId, setup);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { traceId } = params.path;
+ return getTrace(traceId, setup);
},
});
-export const rootTransactionByTraceIdRoute = createRoute({
+const rootTransactionByTraceIdRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces/{traceId}/root_transaction',
params: t.type({
path: t.type({
@@ -57,9 +62,15 @@ export const rootTransactionByTraceIdRoute = createRoute({
}),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const { traceId } = context.params.path;
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params } = resources;
+ const { traceId } = params.path;
+ const setup = await setupRequest(resources);
return getRootTransactionByTraceId(traceId, setup);
},
});
+
+export const traceRouteRepository = createApmServerRouteRepository()
+ .add(tracesByIdRoute)
+ .add(tracesRoute)
+ .add(rootTransactionByTraceIdRoute);
diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts
index f3424a252e409..ebca374db86d7 100644
--- a/x-pack/plugins/apm/server/routes/transactions.ts
+++ b/x-pack/plugins/apm/server/routes/transactions.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
+import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import {
LatencyAggregationType,
latencyAggregationTypeRt,
} from '../../common/latency_aggregation_types';
-import { jsonRt } from '../../common/runtime_types/json_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
@@ -23,7 +23,8 @@ import { getLatencyPeriods } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import {
comparisonRangeRt,
environmentRt,
@@ -35,7 +36,7 @@ import {
* Returns a list of transactions grouped by name
* //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/
*/
-export const transactionGroupsRoute = createRoute({
+const transactionGroupsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups',
params: t.type({
path: t.type({
@@ -49,10 +50,11 @@ export const transactionGroupsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment, kuery, transactionType } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const { environment, kuery, transactionType } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -72,7 +74,7 @@ export const transactionGroupsRoute = createRoute({
},
});
-export const transactionGroupsPrimaryStatisticsRoute = createRoute({
+const transactionGroupsPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics',
params: t.type({
@@ -90,8 +92,9 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -100,7 +103,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
const {
path: { serviceName },
query: { environment, kuery, latencyAggregationType, transactionType },
- } = context.params;
+ } = params;
return getServiceTransactionGroups({
environment,
@@ -109,12 +112,12 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
serviceName,
searchAggregatedTransactions,
transactionType,
- latencyAggregationType: latencyAggregationType as LatencyAggregationType,
+ latencyAggregationType,
});
},
});
-export const transactionGroupsComparisonStatisticsRoute = createRoute({
+const transactionGroupsComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics',
params: t.type({
@@ -135,13 +138,15 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
+ const { params } = resources;
+
const {
path: { serviceName },
query: {
@@ -154,7 +159,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
comparisonStart,
comparisonEnd,
},
- } = context.params;
+ } = params;
return await getServiceTransactionGroupComparisonStatisticsPeriods({
environment,
@@ -165,14 +170,14 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
searchAggregatedTransactions,
transactionType,
numBuckets,
- latencyAggregationType: latencyAggregationType as LatencyAggregationType,
+ latencyAggregationType,
comparisonStart,
comparisonEnd,
});
},
});
-export const transactionLatencyChartsRoute = createRoute({
+const transactionLatencyChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency',
params: t.type({
path: t.type({
@@ -188,10 +193,11 @@ export const transactionLatencyChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const logger = context.logger;
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -200,7 +206,7 @@ export const transactionLatencyChartsRoute = createRoute({
latencyAggregationType,
comparisonStart,
comparisonEnd,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -242,7 +248,7 @@ export const transactionLatencyChartsRoute = createRoute({
},
});
-export const transactionThroughputChartsRoute = createRoute({
+const transactionThroughputChartsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/throughput',
params: t.type({
@@ -258,15 +264,17 @@ export const transactionThroughputChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -284,7 +292,7 @@ export const transactionThroughputChartsRoute = createRoute({
},
});
-export const transactionChartsDistributionRoute = createRoute({
+const transactionChartsDistributionRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/distribution',
params: t.type({
@@ -306,9 +314,10 @@ export const transactionChartsDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -316,7 +325,7 @@ export const transactionChartsDistributionRoute = createRoute({
transactionName,
transactionId = '',
traceId = '',
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -336,7 +345,7 @@ export const transactionChartsDistributionRoute = createRoute({
},
});
-export const transactionChartsBreakdownRoute = createRoute({
+const transactionChartsBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown',
params: t.type({
path: t.type({
@@ -351,15 +360,17 @@ export const transactionChartsBreakdownRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionName,
transactionType,
- } = context.params.query;
+ } = params.query;
return getTransactionBreakdown({
environment,
@@ -372,7 +383,7 @@ export const transactionChartsBreakdownRoute = createRoute({
},
});
-export const transactionChartsErrorRateRoute = createRoute({
+const transactionChartsErrorRateRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate',
params: t.type({
@@ -386,9 +397,10 @@ export const transactionChartsErrorRateRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
const { serviceName } = params.path;
const {
environment,
@@ -416,3 +428,13 @@ export const transactionChartsErrorRateRoute = createRoute({
});
},
});
+
+export const transactionRouteRepository = createApmServerRouteRepository()
+ .add(transactionGroupsRoute)
+ .add(transactionGroupsPrimaryStatisticsRoute)
+ .add(transactionGroupsComparisonStatisticsRoute)
+ .add(transactionLatencyChartsRoute)
+ .add(transactionThroughputChartsRoute)
+ .add(transactionChartsDistributionRoute)
+ .add(transactionChartsBreakdownRoute)
+ .add(transactionChartsErrorRateRoute);
diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts
index 3ba24b4ed5268..0fec88a4326c3 100644
--- a/x-pack/plugins/apm/server/routes/typings.ts
+++ b/x-pack/plugins/apm/server/routes/typings.ts
@@ -5,27 +5,19 @@
* 2.0.
*/
-import t, { Encode, Encoder } from 'io-ts';
import {
CoreSetup,
- KibanaRequest,
RequestHandlerContext,
Logger,
+ KibanaRequest,
+ CoreStart,
} from 'src/core/server';
-import { Observable } from 'rxjs';
-import { RequiredKeys, DeepPartial } from 'utility-types';
-import { SpacesPluginStart } from '../../../spaces/server';
-import { ObservabilityPluginSetup } from '../../../observability/server';
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
-import { SecurityPluginSetup } from '../../../security/server';
-import { MlPluginSetup } from '../../../ml/server';
-import { FetchOptions } from '../../common/fetch_options';
import { APMConfig } from '..';
+import { APMPluginDependencies } from '../types';
-export type HandlerReturn = Record;
-
-interface InspectQueryParam {
- query: { _inspect: boolean };
+export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
+ licensing: LicensingApiRequestHandlerContext;
}
export type InspectResponse = Array<{
@@ -36,141 +28,53 @@ export type InspectResponse = Array<{
esError: Error;
}>;
-export interface RouteParams {
- path?: Record;
- query?: Record;
- body?: any;
+export interface APMRouteCreateOptions {
+ options: {
+ tags: Array<
+ | 'access:apm'
+ | 'access:apm_write'
+ | 'access:ml:canGetJobs'
+ | 'access:ml:canCreateJob'
+ >;
+ };
}
-type WithoutIncompatibleMethods = Omit<
- T,
- 'encode' | 'asEncoder'
-> & { encode: Encode; asEncoder: () => Encoder };
-
-export type RouteParamsRT = WithoutIncompatibleMethods>;
-
-export type RouteHandler<
- TParamsRT extends RouteParamsRT | undefined,
- TReturn extends HandlerReturn
-> = (kibanaContext: {
- context: APMRequestHandlerContext<
- (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) &
- InspectQueryParam
- >;
+export interface APMRouteHandlerResources {
request: KibanaRequest;
-}) => Promise;
-
-interface RouteOptions {
- tags: Array<
- | 'access:apm'
- | 'access:apm_write'
- | 'access:ml:canGetJobs'
- | 'access:ml:canCreateJob'
- >;
-}
-
-export interface Route<
- TEndpoint extends string,
- TRouteParamsRT extends RouteParamsRT | undefined,
- TReturn extends HandlerReturn
-> {
- endpoint: TEndpoint;
- options: RouteOptions;
- params?: TRouteParamsRT;
- handler: RouteHandler;
-}
-
-/**
- * @internal
- */
-export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
- licensing: LicensingApiRequestHandlerContext;
-}
-
-export type APMRequestHandlerContext<
- TRouteParams = {}
-> = ApmPluginRequestHandlerContext & {
- params: TRouteParams & InspectQueryParam;
+ context: ApmPluginRequestHandlerContext;
+ params: {
+ query: {
+ _inspect: boolean;
+ };
+ };
config: APMConfig;
logger: Logger;
- plugins: {
- spaces?: SpacesPluginStart;
- observability?: ObservabilityPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
+ core: {
+ setup: CoreSetup;
+ start: () => Promise;
};
-};
-
-export interface RouteState {
- [endpoint: string]: {
- params?: RouteParams;
- ret: any;
+ plugins: {
+ [key in keyof APMPluginDependencies]: {
+ setup: Required[key]['setup'];
+ start: () => Promise[key]['start']>;
+ };
};
}
-export interface ServerAPI {
- _S: TRouteState;
- add<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
- >(
- route:
- | Route
- | ((core: CoreSetup) => Route)
- ): ServerAPI<
- TRouteState &
- {
- [key in TEndpoint]: {
- params: TRouteParamsRT;
- ret: TReturn & { _inspect?: InspectResponse };
- };
- }
- >;
- init: (
- core: CoreSetup,
- context: {
- config$: Observable;
- logger: Logger;
- plugins: {
- observability?: ObservabilityPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
- };
- }
- ) => void;
-}
-
-type MaybeOptional }> = RequiredKeys<
- T['params']
-> extends never
- ? { params?: T['params'] }
- : { params: T['params'] };
-
-export type MaybeParams<
- TRouteState,
- TEndpoint extends keyof TRouteState & string
-> = TRouteState[TEndpoint] extends { params: t.Any }
- ? MaybeOptional<{
- params: t.OutputOf &
- DeepPartial;
- }>
- : {};
-
-export type Client<
- TRouteState,
- TOptions extends { abortable: boolean } = { abortable: true }
-> = (
- options: Omit<
- FetchOptions,
- 'query' | 'body' | 'pathname' | 'method' | 'signal'
- > & {
- forceCache?: boolean;
- endpoint: TEndpoint;
- } & MaybeParams &
- (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
-) => Promise<
- TRouteState[TEndpoint] extends { ret: any }
- ? TRouteState[TEndpoint]['ret']
- : unknown
->;
+// export type Client<
+// TRouteState,
+// TOptions extends { abortable: boolean } = { abortable: true }
+// > = (
+// options: Omit<
+// FetchOptions,
+// 'query' | 'body' | 'pathname' | 'method' | 'signal'
+// > & {
+// forceCache?: boolean;
+// endpoint: TEndpoint;
+// } & MaybeParams &
+// (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
+// ) => Promise<
+// TRouteState[TEndpoint] extends { ret: any }
+// ? TRouteState[TEndpoint]['ret']
+// : unknown
+// >;
diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts
new file mode 100644
index 0000000000000..cef9eaf2f4fc0
--- /dev/null
+++ b/x-pack/plugins/apm/server/types.ts
@@ -0,0 +1,164 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ValuesType } from 'utility-types';
+import { Observable } from 'rxjs';
+import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server';
+import {
+ PluginSetup as DataPluginSetup,
+ PluginStart as DataPluginStart,
+} from '../../../../src/plugins/data/server';
+import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
+import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
+import {
+ HomeServerPluginSetup,
+ HomeServerPluginStart,
+} from '../../../../src/plugins/home/server';
+import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
+import { ActionsPlugin } from '../../actions/server';
+import { AlertingPlugin } from '../../alerting/server';
+import { CloudSetup } from '../../cloud/server';
+import {
+ PluginSetupContract as FeaturesPluginSetup,
+ PluginStartContract as FeaturesPluginStart,
+} from '../../features/server';
+import {
+ LicensingPluginSetup,
+ LicensingPluginStart,
+} from '../../licensing/server';
+import { MlPluginSetup, MlPluginStart } from '../../ml/server';
+import { ObservabilityPluginSetup } from '../../observability/server';
+import {
+ SecurityPluginSetup,
+ SecurityPluginStart,
+} from '../../security/server';
+import {
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+} from '../../task_manager/server';
+import { APMConfig } from '.';
+import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
+import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
+import { ApmPluginRequestHandlerContext } from './routes/typings';
+
+export interface APMPluginSetup {
+ config$: Observable;
+ getApmIndices: () => ReturnType;
+ createApmEventClient: (params: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: ApmPluginRequestHandlerContext;
+ }) => Promise>;
+}
+
+interface DependencyMap {
+ core: {
+ setup: CoreSetup;
+ start: CoreStart;
+ };
+ spaces: {
+ setup: SpacesPluginSetup;
+ start: SpacesPluginStart;
+ };
+ apmOss: {
+ setup: APMOSSPluginSetup;
+ start: undefined;
+ };
+ home: {
+ setup: HomeServerPluginSetup;
+ start: HomeServerPluginStart;
+ };
+ licensing: {
+ setup: LicensingPluginSetup;
+ start: LicensingPluginStart;
+ };
+ cloud: {
+ setup: CloudSetup;
+ start: undefined;
+ };
+ usageCollection: {
+ setup: UsageCollectionSetup;
+ start: undefined;
+ };
+ taskManager: {
+ setup: TaskManagerSetupContract;
+ start: TaskManagerStartContract;
+ };
+ alerting: {
+ setup: AlertingPlugin['setup'];
+ start: AlertingPlugin['start'];
+ };
+ actions: {
+ setup: ActionsPlugin['setup'];
+ start: ActionsPlugin['start'];
+ };
+ observability: {
+ setup: ObservabilityPluginSetup;
+ start: undefined;
+ };
+ features: {
+ setup: FeaturesPluginSetup;
+ start: FeaturesPluginStart;
+ };
+ security: {
+ setup: SecurityPluginSetup;
+ start: SecurityPluginStart;
+ };
+ ml: {
+ setup: MlPluginSetup;
+ start: MlPluginStart;
+ };
+ data: {
+ setup: DataPluginSetup;
+ start: DataPluginStart;
+ };
+}
+
+const requiredDependencies = [
+ 'features',
+ 'apmOss',
+ 'data',
+ 'licensing',
+ 'triggersActionsUi',
+ 'embeddable',
+ 'infra',
+] as const;
+
+const optionalDependencies = [
+ 'spaces',
+ 'cloud',
+ 'usageCollection',
+ 'taskManager',
+ 'actions',
+ 'alerting',
+ 'observability',
+ 'security',
+ 'ml',
+ 'home',
+ 'maps',
+] as const;
+
+type RequiredDependencies = Pick<
+ DependencyMap,
+ ValuesType & keyof DependencyMap
+>;
+
+type OptionalDependencies = Partial<
+ Pick<
+ DependencyMap,
+ ValuesType & keyof DependencyMap
+ >
+>;
+
+export type APMPluginDependencies = RequiredDependencies & OptionalDependencies;
+
+export type APMPluginSetupDependencies = {
+ [key in keyof APMPluginDependencies]: Required[key]['setup'];
+};
+
+export type APMPluginStartDependencies = {
+ [key in keyof APMPluginDependencies]: Required[key]['start'];
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
index 0bf7d618c33b3..c05c4dcbdddc0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts
@@ -14,6 +14,7 @@ jest.mock('../react_router_helpers', () => ({
import { letBrowserHandleEvent } from '../react_router_helpers';
import {
+ Breadcrumb,
useGenerateBreadcrumbs,
useEuiBreadcrumbs,
useEnterpriseSearchBreadcrumbs,
@@ -40,6 +41,9 @@ describe('useGenerateBreadcrumbs', () => {
{ text: 'Groups', path: '/groups' },
{ text: 'Example Group Name', path: '/groups/{id}' },
{ text: 'Source Prioritization', path: '/groups/{id}/source_prioritization' },
+ // Note: We're still generating a path for the last breadcrumb even though useEuiBreadcrumbs
+ // will not render a link for it. This is because it's easier to keep our last-breadcrumb-specific
+ // logic in one place, & this way we still have a current path if (for some reason) we need it later.
]);
});
@@ -89,48 +93,51 @@ describe('useEuiBreadcrumbs', () => {
},
{
text: 'World',
- href: '/app/enterprise_search/world',
- onClick: expect.any(Function),
+ // Per EUI best practices, the last breadcrumb is inactive/is not a link
},
]);
});
- it('prevents default navigation and uses React Router history on click', () => {
- const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
+ describe('link behavior for non-last breadcrumbs', () => {
+ // Test helper - adds a 2nd dummy breadcrumb so that paths from the first breadcrumb are generated
+ const useEuiBreadcrumb = (breadcrumb: Breadcrumb) =>
+ useEuiBreadcrumbs([breadcrumb, { text: '' }])[0] as any;
- expect(breadcrumb.href).toEqual('/app/enterprise_search/test');
- expect(mockHistory.createHref).toHaveBeenCalled();
+ it('prevents default navigation and uses React Router history on click', () => {
+ const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test' });
- const event = { preventDefault: jest.fn() };
- breadcrumb.onClick(event);
+ expect(breadcrumb.href).toEqual('/app/enterprise_search/test');
+ expect(mockHistory.createHref).toHaveBeenCalled();
- expect(event.preventDefault).toHaveBeenCalled();
- expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled();
- });
+ const event = { preventDefault: jest.fn() };
+ breadcrumb.onClick(event);
- it('does not call createHref if shouldNotCreateHref is passed', () => {
- const breadcrumb = useEuiBreadcrumbs([
- { text: '', path: '/test', shouldNotCreateHref: true },
- ])[0] as any;
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled();
+ });
- expect(breadcrumb.href).toEqual('/test');
- expect(mockHistory.createHref).not.toHaveBeenCalled();
- });
+ it('does not call createHref if shouldNotCreateHref is passed', () => {
+ const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test', shouldNotCreateHref: true });
- it('does not prevent default browser behavior on new tab/window clicks', () => {
- const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/' }])[0] as any;
+ expect(breadcrumb.href).toEqual('/test');
+ expect(mockHistory.createHref).not.toHaveBeenCalled();
+ });
- (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true);
- breadcrumb.onClick();
+ it('does not prevent default browser behavior on new tab/window clicks', () => {
+ const breadcrumb = useEuiBreadcrumb({ text: '', path: '/' });
- expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled();
- });
+ (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true);
+ breadcrumb.onClick();
+
+ expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled();
+ });
- it('does not generate link behavior if path is excluded', () => {
- const breadcrumb = useEuiBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0];
+ it('does not generate link behavior if path is excluded', () => {
+ const breadcrumb = useEuiBreadcrumb({ text: 'Unclickable breadcrumb' });
- expect(breadcrumb.href).toBeUndefined();
- expect(breadcrumb.onClick).toBeUndefined();
+ expect(breadcrumb.href).toBeUndefined();
+ expect(breadcrumb.onClick).toBeUndefined();
+ });
});
});
@@ -164,8 +171,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => {
},
{
text: 'Page 2',
- href: '/app/enterprise_search/page2',
- onClick: expect.any(Function),
},
]);
});
@@ -174,8 +179,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => {
expect(useEnterpriseSearchBreadcrumbs()).toEqual([
{
text: 'Enterprise Search',
- href: '/app/enterprise_search/overview',
- onClick: expect.any(Function),
},
]);
});
@@ -219,8 +222,6 @@ describe('useAppSearchBreadcrumbs', () => {
},
{
text: 'Page 2',
- href: '/app/enterprise_search/app_search/page2',
- onClick: expect.any(Function),
},
]);
});
@@ -234,8 +235,6 @@ describe('useAppSearchBreadcrumbs', () => {
},
{
text: 'App Search',
- href: '/app/enterprise_search/app_search/',
- onClick: expect.any(Function),
},
]);
});
@@ -279,8 +278,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => {
},
{
text: 'Page 2',
- href: '/app/enterprise_search/workplace_search/page2',
- onClick: expect.any(Function),
},
]);
});
@@ -294,8 +291,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => {
},
{
text: 'Workplace Search',
- href: '/app/enterprise_search/workplace_search/',
- onClick: expect.any(Function),
},
]);
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
index 908cc0601ab9c..5855dc6990f6a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts
@@ -24,7 +24,7 @@ import { letBrowserHandleEvent, createHref } from '../react_router_helpers';
* Types
*/
-interface Breadcrumb {
+export interface Breadcrumb {
text: string;
path?: string;
// Used to navigate outside of the React Router basename,
@@ -64,16 +64,20 @@ export const useGenerateBreadcrumbs = (trail: BreadcrumbTrail): Breadcrumbs => {
/**
* Convert IBreadcrumb objects to React-Router-friendly EUI breadcrumb objects
* https://elastic.github.io/eui/#/navigation/breadcrumbs
+ *
+ * NOTE: Per EUI best practices, we remove the link behavior and
+ * generate an inactive breadcrumb for the last breadcrumb in the list.
*/
export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => {
const { navigateToUrl, history } = useValues(KibanaLogic);
const { http } = useValues(HttpLogic);
- return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => {
+ return breadcrumbs.map(({ text, path, shouldNotCreateHref }, i) => {
const breadcrumb: EuiBreadcrumb = { text };
+ const isLastBreadcrumb = i === breadcrumbs.length - 1;
- if (path) {
+ if (path && !isLastBreadcrumb) {
breadcrumb.href = createHref(path, { history, http }, { shouldNotCreateHref });
breadcrumb.onClick = (event) => {
if (letBrowserHandleEvent(event)) return;
diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
index a4cca4455a274..65b853ed5b38f 100644
--- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
+++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
@@ -31,6 +31,7 @@ describe('Fleet - packageToPackagePolicy', () => {
map: [],
lens: [],
ml_module: [],
+ security_rule: [],
},
elasticsearch: {
ingest_pipeline: [],
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index 80fabd51613ae..3bc0d97d64646 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -50,6 +50,7 @@ export enum KibanaAssetType {
indexPattern = 'index_pattern',
map = 'map',
lens = 'lens',
+ securityRule = 'security_rule',
mlModule = 'ml_module',
}
@@ -64,6 +65,7 @@ export enum KibanaSavedObjectType {
map = 'map',
lens = 'lens',
mlModule = 'ml-module',
+ securityRule = 'security-rule',
}
export enum ElasticsearchAssetType {
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
index ea19a330adfee..6ddff968bd3f3 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
@@ -33,6 +33,7 @@ export const AssetTitleMap: Record = {
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',
+ security_rule: 'Security Rule',
ml_module: 'ML Module',
};
@@ -48,6 +49,7 @@ export const AssetIcons: Record = {
visualization: 'visualizeApp',
map: 'emsApp',
lens: 'lensApp',
+ security_rule: 'securityApp',
ml_module: 'mlApp',
};
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
index bfcc40e18fe80..0f2d7b6679bf9 100644
--- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
@@ -38,6 +38,7 @@ const KibanaSavedObjectTypeMapping: Record {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
index 999cf878d07b7..c5b104696aaf4 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
@@ -43,7 +43,7 @@ const tests = [
name: 'coredns',
version: '1.0.1',
},
- // Non existant dataset
+ // Non existent dataset
dataset: 'foo',
filter: (path: string) => {
return true;
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index 20bf349f6b13a..b7dbf1bbe4d87 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -155,11 +155,7 @@ function createMockTimefilter() {
getBounds: jest.fn(() => timeFilter),
getRefreshInterval: () => {},
getRefreshIntervalDefaults: () => {},
- getAutoRefreshFetch$: () => ({
- subscribe: ({ next }: { next: () => void }) => {
- return next;
- },
- }),
+ getAutoRefreshFetch$: () => new Observable(),
};
}
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index dbc10c751a649..39163101fc7bd 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -14,6 +14,7 @@ import { Toast } from 'kibana/public';
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
import { Datatable } from 'src/plugins/expressions/public';
import { EuiBreadcrumb } from '@elastic/eui';
+import { finalize, switchMap, tap } from 'rxjs/operators';
import { downloadMultipleAs } from '../../../../../src/plugins/share/public';
import {
createKbnUrlStateStorage,
@@ -37,6 +38,7 @@ import {
Query,
SavedQuery,
syncQueryStateWithUrl,
+ waitUntilNextSessionCompletes$,
} from '../../../../../src/plugins/data/public';
import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common';
import { LensAppProps, LensAppServices, LensAppState } from './types';
@@ -193,14 +195,19 @@ export function App({
const autoRefreshSubscription = data.query.timefilter.timefilter
.getAutoRefreshFetch$()
- .subscribe({
- next: () => {
+ .pipe(
+ tap(() => {
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
- },
- });
+ }),
+ switchMap((done) =>
+ // best way in lens to estimate that all panels are updated is to rely on search session service state
+ waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
+ )
+ )
+ .subscribe();
const kbnUrlStateStorage = createKbnUrlStateStorage({
history,
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index 9b53e59f96792..cedb648215c0e 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -21,7 +21,7 @@ export type {
YAxisMode,
XYCurveType,
} from './xy_visualization/types';
-export type { DataType } from './types';
+export type { DataType, OperationMetadata } from './types';
export type {
PieVisualizationState,
PieLayerState,
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index 743846d81213c..c1f885d167659 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -18,7 +18,7 @@ const createStartContract = (): Start => {
}),
canUseEditor: jest.fn(() => true),
navigateToPrefilledEditor: jest.fn(),
- getXyVisTypes: jest.fn().mockReturnValue(new Promise(() => visualizationTypes)),
+ getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))),
};
return startContract;
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
index d21bf67a1f51c..5b8fc82ef587b 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
@@ -131,7 +131,7 @@ export const RuntimeMappings: FC = ({ actions, state }) => {
defaultMessage: 'Runtime mappings',
})}
>
-
+
{isPopulatedObject(runtimeMappings) ? (
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
index 0287c2af11a7e..c6cf608fe1e0b 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
@@ -80,7 +80,7 @@ class FieldsService {
if (firstKey !== undefined) {
const field = fc[firstKey];
// add to the list of fields if the field type can be used by ML
- if (supportedTypes.includes(field.type) === true) {
+ if (supportedTypes.includes(field.type) === true && field.metadata_field !== true) {
fields.push({
id: k,
name: k,
diff --git a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx
index b1ca3c614fc70..fce1cde38f587 100644
--- a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx
@@ -5,53 +5,38 @@
* 2.0.
*/
-import React from 'react';
-import { EuiPanel } from '@elastic/eui';
-import { EuiFlexGroup } from '@elastic/eui';
-import { EuiFlexItem } from '@elastic/eui';
-import { EuiTitle } from '@elastic/eui';
+import { EuiCard, EuiLink, EuiTextColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { EuiText } from '@elastic/eui';
-import { EuiLink } from '@elastic/eui';
+import React from 'react';
import { usePluginContext } from '../../../hooks/use_plugin_context';
export function FleetPanel() {
const { core } = usePluginContext();
return (
-
-
-
-
-
- {i18n.translate('xpack.observability.fleet.title', {
- defaultMessage: 'Have you seen our new Fleet?',
- })}
-
-
-
-
-
- {i18n.translate('xpack.observability.fleet.text', {
- defaultMessage:
- 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.',
- })}
-
-
-
-
- {i18n.translate('xpack.observability.fleet.button', {
- defaultMessage: 'Try Fleet Beta',
- })}
-
-
-
-
+ description={
+
+ {i18n.translate('xpack.observability.fleet.text', {
+ defaultMessage:
+ 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.',
+ })}
+
+ }
+ footer={
+
+ {i18n.translate('xpack.observability.fleet.button', {
+ defaultMessage: 'Try Fleet Beta',
+ })}
+
+ }
+ title={i18n.translate('xpack.observability.fleet.title', {
+ defaultMessage: 'Have you seen our new Fleet?',
+ })}
+ />
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
similarity index 82%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
index a31679c61a4ab..3fcf98f712bef 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { ConfigProps, DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { buildPhraseFilter } from './utils';
-import { OperationType } from '../../../../../../lens/public';
+import { ConfigProps, DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { buildPhraseFilter } from '../utils';
+import { OperationType } from '../../../../../../../lens/public';
export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
return {
@@ -20,7 +20,7 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'transaction.duration.us',
label: 'Latency',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
similarity index 82%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
index 32cae2167ddf0..c0f3d6dc9b010 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { ConfigProps, DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { buildPhraseFilter } from './utils';
-import { OperationType } from '../../../../../../lens/public';
+import { ConfigProps, DataSeries } from '../../types';
+import { FieldLabels } from '../constants/constants';
+import { buildPhraseFilter } from '../utils';
+import { OperationType } from '../../../../../../../lens/public';
export function getServiceThroughputLensConfig({
seriesId,
@@ -23,7 +23,7 @@ export function getServiceThroughputLensConfig({
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'transaction.duration.us',
label: 'Throughput',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
similarity index 80%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index aa3ac2fa64317..ed849c1eb47b3 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -5,14 +5,8 @@
* 2.0.
*/
-import { AppDataType, ReportViewTypeId } from '../types';
-import {
- CLS_FIELD,
- FCP_FIELD,
- FID_FIELD,
- LCP_FIELD,
- TBT_FIELD,
-} from './data/elasticsearch_fieldnames';
+import { AppDataType, ReportViewTypeId } from '../../types';
+import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
export const FieldLabels: Record = {
'user_agent.name': 'Browser family',
@@ -24,10 +18,10 @@ export const FieldLabels: Record = {
'service.name': 'Service Name',
'service.environment': 'Environment',
- [LCP_FIELD]: 'Largest contentful paint',
- [FCP_FIELD]: 'First contentful paint',
- [TBT_FIELD]: 'Total blocking time',
- [FID_FIELD]: 'First input delay',
+ [LCP_FIELD]: 'Largest contentful paint (Seconds)',
+ [FCP_FIELD]: 'First contentful paint (Seconds)',
+ [TBT_FIELD]: 'Total blocking time (Seconds)',
+ [FID_FIELD]: 'First input delay (Seconds)',
[CLS_FIELD]: 'Cumulative layout shift',
'monitor.id': 'Monitor Id',
@@ -38,6 +32,7 @@ export const FieldLabels: Record = {
'monitor.name': 'Monitor name',
'monitor.type': 'Monitor Type',
'url.port': 'Port',
+ 'url.full': 'Url',
tags: 'Tags',
// custom
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts
similarity index 100%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts
new file mode 100644
index 0000000000000..63661f0d5a996
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './constants';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
similarity index 100%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 85d48ef638d44..2c5b4ebea0ab3 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -6,16 +6,16 @@
*/
import { ReportViewTypes } from '../types';
-import { getPerformanceDistLensConfig } from './performance_dist_config';
-import { getMonitorDurationConfig } from './monitor_duration_config';
-import { getServiceLatencyLensConfig } from './service_latency_config';
-import { getMonitorPingsConfig } from './monitor_pings_config';
-import { getServiceThroughputLensConfig } from './service_throughput_config';
-import { getKPITrendsLensConfig } from './kpi_trends_config';
-import { getCPUUsageLensConfig } from './cpu_usage_config';
-import { getMemoryUsageLensConfig } from './memory_usage_config';
-import { getNetworkActivityLensConfig } from './network_activity_config';
-import { getLogsFrequencyLensConfig } from './logs_frequency_config';
+import { getPerformanceDistLensConfig } from './rum/performance_dist_config';
+import { getMonitorDurationConfig } from './synthetics/monitor_duration_config';
+import { getServiceLatencyLensConfig } from './apm/service_latency_config';
+import { getMonitorPingsConfig } from './synthetics/monitor_pings_config';
+import { getServiceThroughputLensConfig } from './apm/service_throughput_config';
+import { getKPITrendsLensConfig } from './rum/kpi_trends_config';
+import { getCPUUsageLensConfig } from './metrics/cpu_usage_config';
+import { getMemoryUsageLensConfig } from './metrics/memory_usage_config';
+import { getNetworkActivityLensConfig } from './metrics/network_activity_config';
+import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
interface Props {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index dcfaed938cc0f..139f3ab0d82ed 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -8,9 +8,8 @@
import { LensAttributes } from './lens_attributes';
import { mockIndexPattern } from '../rtl_helpers';
import { getDefaultConfigs } from './default_configs';
-import { sampleAttribute } from './data/sample_attribute';
-import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames';
-import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames';
+import { sampleAttribute } from './test_data/sample_attribute';
+import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
describe('Lens Attribute', () => {
const reportViewConfig = getDefaultConfigs({
@@ -93,7 +92,7 @@ describe('Lens Attribute', () => {
expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -129,7 +128,7 @@ describe('Lens Attribute', () => {
expect(lnsAttr.getXAxis()).toEqual({
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -154,7 +153,7 @@ describe('Lens Attribute', () => {
'x-axis-column': {
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -318,7 +317,7 @@ describe('Lens Attribute', () => {
'x-axis-column': {
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -363,7 +362,7 @@ describe('Lens Attribute', () => {
'x-axis-column': {
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
operationType: 'range',
params: {
maxBars: 'auto',
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
similarity index 90%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
index 68e5e697d2f9d..8a27d7ddd428b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
interface Props {
seriesId: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
similarity index 82%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
index 5a4fb2aa3a6a5..6214975d8f1dd 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { OperationType } from '../../../../../../lens/public';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,7 +23,7 @@ export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'system.cpu.user.pct',
label: 'CPU Usage %',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
similarity index 82%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
index 579372ed86fa7..6f46c175f7882 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { OperationType } from '../../../../../../lens/public';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,7 +23,7 @@ export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'system.memory.used.pct',
label: 'Memory Usage %',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
similarity index 81%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
index 63cdd0ec8bd60..1bc9fed9c3f80 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { OperationType } from '../../../../../../lens/public';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,7 +23,7 @@ export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'system.memory.used.pct',
},
hasMetricType: true,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts
new file mode 100644
index 0000000000000..f1fc5f310b8ef
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FieldFormat } from '../../types';
+import {
+ FCP_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
+ TBT_FIELD,
+ TRANSACTION_DURATION,
+} from '../constants/elasticsearch_fieldnames';
+
+export const rumFieldFormats: FieldFormat[] = [
+ {
+ field: TRANSACTION_DURATION,
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'microseconds',
+ outputFormat: 'asSeconds',
+ showSuffix: true,
+ outputPrecision: 1,
+ },
+ },
+ },
+ {
+ field: FCP_FIELD,
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'milliseconds',
+ outputFormat: 'asSeconds',
+ showSuffix: true,
+ },
+ },
+ },
+ {
+ field: LCP_FIELD,
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'milliseconds',
+ outputFormat: 'asSeconds',
+ showSuffix: true,
+ },
+ },
+ },
+ {
+ field: TBT_FIELD,
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'milliseconds',
+ outputFormat: 'asSeconds',
+ showSuffix: true,
+ },
+ },
+ },
+ {
+ field: FID_FIELD,
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'milliseconds',
+ outputFormat: 'asSeconds',
+ showSuffix: true,
+ },
+ },
+ },
+];
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
similarity index 90%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
index a967a8824bca7..a1a3acd51f89c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { ConfigProps, DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { buildPhraseFilter } from './utils';
+import { ConfigProps, DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
PROCESSOR_EVENT,
@@ -18,7 +18,7 @@ import {
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
-} from './data/elasticsearch_fieldnames';
+} from '../constants/elasticsearch_fieldnames';
export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
return {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
similarity index 90%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
index 41617304c9f3d..7005dea29d60d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { ConfigProps, DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { buildPhraseFilter } from './utils';
+import { ConfigProps, DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
+import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
@@ -24,7 +24,7 @@ import {
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
-} from './data/elasticsearch_fieldnames';
+} from '../constants/elasticsearch_fieldnames';
export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
return {
@@ -80,7 +80,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP
labels: {
...FieldLabels,
[SERVICE_NAME]: 'Web Application',
- [TRANSACTION_DURATION]: 'Page load time',
+ [TRANSACTION_DURATION]: 'Page load time (Seconds)',
},
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts
new file mode 100644
index 0000000000000..4f036f0b9be65
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FieldFormat } from '../../types';
+
+export const syntheticsFieldFormats: FieldFormat[] = [
+ {
+ field: 'monitor.duration.us',
+ format: {
+ id: 'duration',
+ params: {
+ inputFormat: 'microseconds',
+ outputFormat: 'asMilliseconds',
+ outputPrecision: 0,
+ },
+ },
+ },
+];
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
similarity index 83%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
index aa9b8b94c6d86..f0ec3f0c31bef 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
-import { OperationType } from '../../../../../../lens/public';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants/constants';
+import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,7 +23,7 @@ export function getMonitorDurationConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'avg' as OperationType,
+ operationType: 'average' as OperationType,
sourceField: 'monitor.duration.us',
label: 'Monitor duration (ms)',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
similarity index 92%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
index 72968626e934b..40c9f5750fb4d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
-import { DataSeries } from '../types';
-import { FieldLabels } from './constants';
+import { DataSeries } from '../../types';
+import { FieldLabels } from '../constants';
interface Props {
seriesId: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
similarity index 98%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
index 9b299e7d70bcc..ffce81207472f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
@@ -21,7 +21,7 @@ export const sampleAttribute = {
columns: {
'x-axis-column': {
sourceField: 'transaction.duration.us',
- label: 'Page load time',
+ label: 'Page load time (Seconds)',
dataType: 'number',
operationType: 'range',
isBucketed: true,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json
similarity index 100%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
index 38b8ce81b2acd..c885673134786 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
import rison, { RisonValue } from 'rison-node';
-import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage';
+import type { AllSeries, AllShortSeries } from '../hooks/use_url_storage';
import type { SeriesUrl } from '../types';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
-import { URL_KEYS } from './url_constants';
+import { URL_KEYS } from './constants/url_constants';
export function convertToShortUrl(series: SeriesUrl) {
const {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index b90d5115bc41e..257eb3a739f0f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -10,7 +10,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/dom';
import { render, mockUrlStorage, mockCore } from './rtl_helpers';
import { ExploratoryView } from './exploratory_view';
import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils';
-import * as obsvInd from '../../../utils/observability_index_patterns';
+import * as obsvInd from './utils/observability_index_patterns';
describe('ExploratoryView', () => {
beforeEach(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index b3ad107bbe0e2..0e7bc80e8659c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
import { SeriesEditor } from './series_editor/series_editor';
-import { useUrlStorage } from './hooks/use_url_strorage';
+import { useUrlStorage } from './hooks/use_url_storage';
import { useLensAttributes } from './hooks/use_lens_attributes';
import { EmptyView } from './components/empty_view';
import { useIndexPatternContext } from './hooks/use_default_index_pattern';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index bda3566c76602..17f06436c8535 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -12,7 +12,7 @@ import { TypedLensByValueInput } from '../../../../../../lens/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
import { DataViewLabels } from '../configurations/constants';
-import { useUrlStorage } from '../hooks/use_url_strorage';
+import { useUrlStorage } from '../hooks/use_url_storage';
interface Props {
seriesId: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
index 04cbb4a4ddb18..7ead7d5e3cfad 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
@@ -10,7 +10,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { AppDataType } from '../types';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
-import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns';
+import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns';
export interface IIndexPatternContext {
indexPattern: IndexPattern;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
index 9f462790e8d37..76fd64ef86736 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
@@ -8,12 +8,9 @@ import { useFetcher } from '../../../..';
import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
-import { AllShortSeries } from './use_url_strorage';
+import { AllShortSeries } from './use_url_storage';
import { ReportToDataTypeMap } from '../configurations/constants';
-import {
- DataType,
- ObservabilityIndexPatterns,
-} from '../../../../utils/observability_index_patterns';
+import { DataType, ObservabilityIndexPatterns } from '../utils/observability_index_patterns';
export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => {
const {
@@ -30,7 +27,7 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => {
const firstSeries = allSeries[firstSeriesId];
- const { data: indexPattern } = useFetcher(() => {
+ const { data: indexPattern, error } = useFetcher(() => {
const obsvIndexP = new ObservabilityIndexPatterns(data);
let reportType: DataType = 'apm';
if (firstSeries?.rt) {
@@ -40,5 +37,9 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => {
return obsvIndexP.getIndexPattern(reportType);
}, [firstSeries?.rt, data]);
+ if (error) {
+ throw error;
+ }
+
return indexPattern;
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 1c735009f66f9..274542380c137 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -8,7 +8,7 @@
import { useMemo } from 'react';
import { TypedLensByValueInput } from '../../../../../../lens/public';
import { LensAttributes } from '../configurations/lens_attributes';
-import { useUrlStorage } from './use_url_strorage';
+import { useUrlStorage } from './use_url_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
index 35247180c2ee5..34f0a7c1a7f86 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useUrlStorage } from './use_url_strorage';
+import { useUrlStorage } from './use_url_storage';
import { UrlFilter } from '../types';
export interface UpdateFilter {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
similarity index 97%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
index d38429703b709..6256b3b134f8c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
@@ -10,7 +10,7 @@ import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_
import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types';
import { convertToShortUrl } from '../configurations/utils';
import { OperationType, SeriesType } from '../../../../../../lens/public';
-import { URL_KEYS } from '../configurations/url_constants';
+import { URL_KEYS } from '../configurations/constants/url_constants';
export const UrlStorageContext = createContext(null);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
index dc47a0f075fe6..f903c4d7d44fb 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
@@ -18,7 +18,7 @@ import {
createKbnUrlStateStorage,
withNotifyOnErrors,
} from '../../../../../../../src/plugins/kibana_utils/public/';
-import { UrlStorageContextProvider } from './hooks/use_url_strorage';
+import { UrlStorageContextProvider } from './hooks/use_url_storage';
import { useInitExploratoryView } from './hooks/use_init_exploratory_view';
import { WithHeaderLayout } from '../../app/layout/with_header';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index 112bfcc3ccb58..b826409dd9e3a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -23,20 +23,20 @@ import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
import { lensPluginMock } from '../../../../../lens/public/mocks';
import { IndexPatternContextProvider } from './hooks/use_default_index_pattern';
-import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage';
+import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_storage';
import {
withNotifyOnErrors,
createKbnUrlStateStorage,
} from '../../../../../../../src/plugins/kibana_utils/public';
import * as fetcherHook from '../../../hooks/use_fetcher';
-import * as useUrlHook from './hooks/use_url_strorage';
+import * as useUrlHook from './hooks/use_url_storage';
import * as useSeriesFilterHook from './hooks/use_series_filters';
import * as useHasDataHook from '../../../hooks/use_has_data';
import * as useValuesListHook from '../../../hooks/use_values_list';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub';
-import indexPatternData from './configurations/data/test_index_pattern.json';
+import indexPatternData from './configurations/test_data/test_index_pattern.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
index d33d8515d3bee..039cdfc9b73f5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { mockUrlStorage, render } from '../../rtl_helpers';
import { dataTypes, DataTypesCol } from './data_types_col';
-import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY } from '../../hooks/use_url_storage';
describe('DataTypesCol', function () {
it('should render properly', function () {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
index 7ea44e66a721a..b6464bbe3c6ed 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { AppDataType } from '../../types';
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
-import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage';
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
{ id: 'synthetics', label: 'Synthetic Monitoring' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
index dba660fff9c36..553aff57ad491 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
@@ -10,9 +10,9 @@ import { fireEvent, screen } from '@testing-library/react';
import { render } from '../../../../../utils/test_helper';
import { getDefaultConfigs } from '../../configurations/default_configs';
import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers';
-import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY } from '../../hooks/use_url_storage';
import { ReportBreakdowns } from './report_breakdowns';
-import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames';
+import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Series Builder ReportBreakdowns', function () {
const dataViewSeries = getDefaultConfigs({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
index 7667cea417a52..619e2ec4fe9b0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { Breakdowns } from '../../series_editor/columns/breakdowns';
-import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY } from '../../hooks/use_url_storage';
import { DataSeries } from '../../types';
export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
index 2fda581154166..104a8fcefb49f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
@@ -9,9 +9,9 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { getDefaultConfigs } from '../../configurations/default_configs';
import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers';
-import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY } from '../../hooks/use_url_storage';
import { ReportDefinitionCol } from './report_definition_col';
-import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames';
+import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Series Builder ReportDefinitionCol', function () {
const dataViewSeries = getDefaultConfigs({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
index ce11c869de0ab..b907efb57d5c2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
-import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage';
import { CustomReportField } from '../custom_report_field';
import FieldValueSuggestions from '../../../field_value_suggestions';
import { DataSeries } from '../../types';
@@ -67,6 +67,7 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe
{rtd?.[field] && (
;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx
index 6039fd4cba280..4d5033eca241b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiSuperSelect } from '@elastic/eui';
-import { useUrlStorage } from '../hooks/use_url_strorage';
+import { useUrlStorage } from '../hooks/use_url_storage';
import { ReportDefinition } from '../types';
interface Props {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
index 983c18af031d0..053f301529635 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
@@ -16,7 +16,7 @@ import { ReportTypesCol } from './columns/report_types_col';
import { ReportDefinitionCol } from './columns/report_definition_col';
import { ReportFilters } from './columns/report_filters';
import { ReportBreakdowns } from './columns/report_breakdowns';
-import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage';
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
import { getDefaultConfigs } from '../configurations/default_configs';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
index 71e3317ad6db8..922d33ffd39ac 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
@@ -8,7 +8,7 @@
import { EuiSuperDatePicker } from '@elastic/eui';
import React, { useEffect } from 'react';
import { useHasData } from '../../../../hooks/use_has_data';
-import { useUrlStorage } from '../hooks/use_url_strorage';
+import { useUrlStorage } from '../hooks/use_url_storage';
import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
export interface TimePickerTime {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
index 654a93a08a7c8..0824f13e6b3fe 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
@@ -9,9 +9,9 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { Breakdowns } from './breakdowns';
import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers';
-import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY } from '../../hooks/use_url_storage';
import { getDefaultConfigs } from '../../configurations/default_configs';
-import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames';
+import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Breakdowns', function () {
const dataViewSeries = getDefaultConfigs({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
index 0d34d7245725a..5561779daa8c4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { EuiSuperSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FieldLabels } from '../../configurations/constants';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
interface Props {
seriesId: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
index 017655053eef2..f83630cff414a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
@@ -19,7 +19,7 @@ import styled from 'styled-components';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
import { useFetcher } from '../../../../..';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
import { SeriesType } from '../../../../../../../lens/public';
export function SeriesChartTypes({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
index edd5546f13940..530b8dee3a4d2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { FilterExpanded } from './filter_expanded';
import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers';
-import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames';
+import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('FilterExpanded', function () {
it('should render properly', async function () {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
index 280912dd0902f..3e6d7890f4c81 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
@@ -14,7 +14,7 @@ import {
EuiFilterGroup,
} from '@elastic/eui';
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
import { UrlFilter } from '../../types';
import { FilterValueButton } from './filter_value_btn';
import { useValuesList } from '../../../../../hooks/use_values_list';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
index 7f76c9ea999ee..befbb3b74d6d7 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
@@ -12,7 +12,7 @@ import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from '
import {
USER_AGENT_NAME,
USER_AGENT_VERSION,
-} from '../../configurations/data/elasticsearch_fieldnames';
+} from '../../configurations/constants/elasticsearch_fieldnames';
describe('FilterValueButton', function () {
it('should render properly', async function () {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
index 42cdfd595e66b..efccb351c2619 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
@@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { EuiFilterButton, hexToRgb } from '@elastic/eui';
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
import { useSeriesFilters } from '../../hooks/use_series_filters';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import FieldValueSuggestions from '../../../field_value_suggestions';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
index e01e371b5eeeb..fa4202d2c30ad 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
@@ -8,12 +8,12 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
import { OperationType } from '../../../../../../../lens/public';
const toggleButtons = [
{
- id: `avg`,
+ id: `average`,
label: i18n.translate('xpack.observability.expView.metricsSelect.average', {
defaultMessage: 'Average',
}),
@@ -49,7 +49,7 @@ export function MetricSelection({
const [isOpen, setIsOpen] = useState(false);
- const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg');
+ const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'average');
const onChange = (optionId: OperationType) => {
setToggleIdSelected(optionId);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
index 67aebed943326..aaaa02c7c5697 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import { DataSeries } from '../../types';
-import { useUrlStorage } from '../../hooks/use_url_strorage';
+import { useUrlStorage } from '../../hooks/use_url_storage';
interface Props {
series: DataSeries;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
index 24b65d2adb38e..c9bb44cfd8cca 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
@@ -17,9 +17,9 @@ import {
} from '@elastic/eui';
import { FilterExpanded } from './filter_expanded';
import { DataSeries } from '../../types';
-import { FieldLabels } from '../../configurations/constants';
+import { FieldLabels } from '../../configurations/constants/constants';
import { SelectedFilters } from '../selected_filters';
-import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage';
interface Props {
seriesId: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
index 5770a7e209f06..a38b50d610c75 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
@@ -10,8 +10,8 @@ import { screen, waitFor } from '@testing-library/react';
import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers';
import { SelectedFilters } from './selected_filters';
import { getDefaultConfigs } from '../configurations/default_configs';
-import { NEW_SERIES_KEY } from '../hooks/use_url_strorage';
-import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames';
+import { NEW_SERIES_KEY } from '../hooks/use_url_storage';
+import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
describe('SelectedFilters', function () {
const dataViewSeries = getDefaultConfigs({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
index be8b1feb4d723..34e69f688eaaf 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
@@ -7,7 +7,7 @@
import React, { Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage';
import { FilterLabel } from '../components/filter_label';
import { DataSeries, UrlFilter } from '../types';
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index 2d423c9aee3fc..2d8bd12904fbd 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -13,7 +13,7 @@ import { ActionsCol } from './columns/actions_col';
import { Breakdowns } from './columns/breakdowns';
import { DataSeries } from '../types';
import { SeriesBuilder } from '../series_builder/series_builder';
-import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
+import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { DatePickerCol } from './columns/date_picker_col';
import { RemoveSeries } from './columns/remove_series';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 444e0ddaecb4a..d673fc4d6f6ee 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -87,3 +87,22 @@ export interface ConfigProps {
}
export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm';
+
+type FormatType = 'duration' | 'number';
+type InputFormat = 'microseconds' | 'milliseconds' | 'seconds';
+type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize';
+
+export interface FieldFormatParams {
+ inputFormat: InputFormat;
+ outputFormat: OutputFormat;
+ outputPrecision?: number;
+ showSuffix?: boolean;
+}
+
+export interface FieldFormat {
+ field: string;
+ format: {
+ id: FormatType;
+ params: FieldFormatParams;
+ };
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts
new file mode 100644
index 0000000000000..b6f544db2a319
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { indexPatternList, ObservabilityIndexPatterns } from './observability_index_patterns';
+import { mockCore, mockIndexPattern } from '../rtl_helpers';
+import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public';
+
+const fieldFormats = {
+ 'transaction.duration.us': {
+ id: 'duration',
+ params: {
+ inputFormat: 'microseconds',
+ outputFormat: 'asSeconds',
+ outputPrecision: 1,
+ showSuffix: true,
+ },
+ },
+ 'transaction.experience.fid': {
+ id: 'duration',
+ params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true },
+ },
+ 'transaction.experience.tbt': {
+ id: 'duration',
+ params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true },
+ },
+ 'transaction.marks.agent.firstContentfulPaint': {
+ id: 'duration',
+ params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true },
+ },
+ 'transaction.marks.agent.largestContentfulPaint': {
+ id: 'duration',
+ params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true },
+ },
+};
+
+describe('ObservabilityIndexPatterns', function () {
+ const { data } = mockCore();
+ data!.indexPatterns.get = jest.fn().mockReturnValue({ title: 'index-*' });
+ data!.indexPatterns.createAndSave = jest.fn().mockReturnValue({ id: indexPatternList.rum });
+ data!.indexPatterns.updateSavedObject = jest.fn();
+
+ it('should return index pattern for app', async function () {
+ const obsv = new ObservabilityIndexPatterns(data!);
+
+ const indexP = await obsv.getIndexPattern('rum');
+
+ expect(indexP).toEqual({ title: 'index-*' });
+
+ expect(data?.indexPatterns.get).toHaveBeenCalledWith(indexPatternList.rum);
+ expect(data?.indexPatterns.get).toHaveBeenCalledTimes(1);
+ });
+
+ it('should creates missing index pattern', async function () {
+ data!.indexPatterns.get = jest.fn().mockImplementation(() => {
+ throw new SavedObjectNotFound('index_pattern');
+ });
+
+ const obsv = new ObservabilityIndexPatterns(data!);
+
+ const indexP = await obsv.getIndexPattern('rum');
+
+ expect(indexP).toEqual({ id: indexPatternList.rum });
+
+ expect(data?.indexPatterns.createAndSave).toHaveBeenCalledWith({
+ fieldFormats,
+ id: 'rum_static_index_pattern_id',
+ timeFieldName: '@timestamp',
+ title: '(rum-data-view)*,apm-*',
+ });
+ expect(data?.indexPatterns.createAndSave).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return getFieldFormats', function () {
+ const obsv = new ObservabilityIndexPatterns(data!);
+
+ expect(obsv.getFieldFormats('rum')).toEqual(fieldFormats);
+ });
+
+ it('should validate field formats', async function () {
+ mockIndexPattern.getFormatterForField = jest.fn().mockReturnValue({ params: () => {} });
+
+ const obsv = new ObservabilityIndexPatterns(data!);
+
+ await obsv.validateFieldFormats('rum', mockIndexPattern);
+
+ expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledTimes(1);
+ expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledWith(
+ expect.objectContaining({ fieldFormatMap: fieldFormats })
+ );
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
new file mode 100644
index 0000000000000..e0a2941b24d3c
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public';
+import {
+ DataPublicPluginStart,
+ IndexPattern,
+ FieldFormat as IFieldFormat,
+ IndexPatternSpec,
+} from '../../../../../../../../src/plugins/data/public';
+import { rumFieldFormats } from '../configurations/rum/field_formats';
+import { syntheticsFieldFormats } from '../configurations/synthetics/field_formats';
+import { FieldFormat, FieldFormatParams } from '../types';
+
+const appFieldFormats: Record = {
+ rum: rumFieldFormats,
+ apm: null,
+ logs: null,
+ metrics: null,
+ synthetics: syntheticsFieldFormats,
+};
+
+function getFieldFormatsForApp(app: DataType) {
+ return appFieldFormats[app];
+}
+
+export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum';
+
+export const indexPatternList: Record = {
+ synthetics: 'synthetics_static_index_pattern_id',
+ apm: 'apm_static_index_pattern_id',
+ rum: 'rum_static_index_pattern_id',
+ logs: 'logs_static_index_pattern_id',
+ metrics: 'metrics_static_index_pattern_id',
+};
+
+const appToPatternMap: Record = {
+ synthetics: '(synthetics-data-view)*,heartbeat-*,synthetics-*',
+ apm: 'apm-*',
+ rum: '(rum-data-view)*,apm-*',
+ logs: 'logs-*,filebeat-*',
+ metrics: 'metrics-*,metricbeat-*',
+};
+
+export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) {
+ return (
+ param1?.inputFormat === param2?.inputFormat &&
+ param1?.outputFormat === param2?.outputFormat &&
+ param1?.showSuffix === param2?.showSuffix &&
+ param2?.outputPrecision === param1?.outputPrecision
+ );
+}
+
+export class ObservabilityIndexPatterns {
+ data?: DataPublicPluginStart;
+
+ constructor(data: DataPublicPluginStart) {
+ this.data = data;
+ }
+
+ async createIndexPattern(app: DataType) {
+ if (!this.data) {
+ throw new Error('data is not defined');
+ }
+
+ const pattern = appToPatternMap[app];
+
+ return await this.data.indexPatterns.createAndSave({
+ title: pattern,
+ id: indexPatternList[app],
+ timeFieldName: '@timestamp',
+ fieldFormats: this.getFieldFormats(app),
+ });
+ }
+ // we want to make sure field formats remain same
+ async validateFieldFormats(app: DataType, indexPattern: IndexPattern) {
+ const defaultFieldFormats = getFieldFormatsForApp(app);
+ if (defaultFieldFormats && defaultFieldFormats.length > 0) {
+ let isParamsDifferent = false;
+ defaultFieldFormats.forEach(({ field, format }) => {
+ const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!);
+ const params = fieldFormat.params();
+ if (!isParamsSame(params, format.params)) {
+ indexPattern.setFieldFormat(field, format);
+ isParamsDifferent = true;
+ }
+ });
+ if (isParamsDifferent) {
+ await this.data?.indexPatterns.updateSavedObject(indexPattern);
+ }
+ }
+ }
+
+ getFieldFormats(app: DataType) {
+ const fieldFormatMap: IndexPatternSpec['fieldFormats'] = {};
+
+ (appFieldFormats?.[app] ?? []).forEach(({ field, format }) => {
+ fieldFormatMap[field] = format;
+ });
+
+ return fieldFormatMap;
+ }
+
+ async getIndexPattern(app: DataType): Promise {
+ if (!this.data) {
+ throw new Error('data is not defined');
+ }
+ try {
+ const indexPattern = await this.data?.indexPatterns.get(indexPatternList[app]);
+
+ // this is intentional a non blocking call, so no await clause
+ this.validateFieldFormats(app, indexPattern);
+ return indexPattern;
+ } catch (e: unknown) {
+ if (e instanceof SavedObjectNotFound) {
+ return await this.createIndexPattern(app || 'apm');
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
index a44aab2da85be..d14039ba173ac 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
@@ -76,6 +76,7 @@ export function FieldValueSelection({
{
+ const pluginContextValue = ({
+ appMountParameters: { setHeaderActionMenu: () => {} },
+ core: {
+ http: {
+ basePath: {
+ prepend: () => '',
+ },
+ },
+ },
+ } as unknown) as PluginContextValue;
+ return (
+