From d110e65624605cba404b4365df602693025b32b6 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Mon, 15 Jun 2020 21:54:04 -0600 Subject: [PATCH 01/68] Sort filter fields by items that start with typed characters. (#68585) --- .../public/ui/filter_bar/filter_editor/generic_combo_box.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx index 66adbfe9a6fc3..c66518c18a9ea 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx @@ -59,6 +59,7 @@ export function GenericComboBox(props: GenericComboBoxProps) { options={euiOptions} selectedOptions={selectedEuiOptions} onChange={onComboBoxChange} + sortMatchesBy="startsWith" {...otherProps} /> ); From 7e328417ab891cce72df2b25bfa56b4119a2de3f Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 15 Jun 2020 22:15:31 -0700 Subject: [PATCH 02/68] [kbn/optimizer] share all plugin bundles (#68986) Co-authored-by: spalger Co-authored-by: Elastic Machine --- .eslintrc.js | 1 + NOTICE.txt | 5 + ...e-server.pluginmanifest.extrapublicdirs.md | 17 ++ ...ibana-plugin-core-server.pluginmanifest.md | 1 + examples/demo_search/kibana.json | 3 +- examples/embeddable_examples/kibana.json | 3 +- examples/url_generators_examples/kibana.json | 5 +- packages/kbn-optimizer/README.md | 12 ++ packages/kbn-optimizer/package.json | 1 + .../mock_repo/plugins/bar/public/index.ts | 2 +- .../kbn-optimizer/src/common/bundle.test.ts | 10 +- packages/kbn-optimizer/src/common/bundle.ts | 22 ++- .../kbn-optimizer/src/common/bundle_cache.ts | 5 + .../kbn-optimizer/src/common/bundle_refs.ts | 130 ++++++++++++++ packages/kbn-optimizer/src/common/index.ts | 1 + .../basic_optimization.test.ts.snap | 17 +- .../basic_optimization.test.ts | 10 +- .../integration_tests/bundle_cache.test.ts | 53 ++++++ .../watch_bundles_for_changes.test.ts | 4 +- .../assign_bundles_to_workers.test.ts | 2 +- .../src/optimizer/bundle_cache.ts | 33 +++- .../kbn-optimizer/src/optimizer/cache_keys.ts | 12 +- .../src/optimizer/get_plugin_bundles.test.ts | 11 +- .../src/optimizer/get_plugin_bundles.ts | 2 +- .../optimizer/kibana_platform_plugins.test.ts | 4 + .../src/optimizer/kibana_platform_plugins.ts | 16 ++ .../src/optimizer/observe_worker.ts | 8 +- .../src/optimizer/optimizer_config.ts | 24 ++- .../src/worker/bundle_ref_module.ts | 67 +++++++ .../src/worker/bundle_refs_plugin.ts | 165 ++++++++++++++++++ .../src/worker/entry_point_creator.ts | 28 +++ .../kbn-optimizer/src/worker/run_compilers.ts | 71 ++++---- .../kbn-optimizer/src/worker/run_worker.ts | 12 +- .../src/worker/webpack.config.ts | 127 ++++---------- src/core/public/plugins/plugin_reader.test.ts | 33 ++-- src/core/public/plugins/plugin_reader.ts | 14 +- .../discovery/plugin_manifest_parser.ts | 21 ++- src/core/server/plugins/types.ts | 8 + src/core/server/server.api.md | 8 +- .../ui/ui_render/bootstrap/app_bootstrap.js | 4 +- .../bootstrap/kbn_bundles_loader_source.js | 51 ++++++ .../ui/ui_render/bootstrap/template.js.hbs | 8 +- src/legacy/ui/ui_render/ui_render_mixin.js | 12 +- src/plugins/data/kibana.json | 3 +- src/plugins/embeddable/kibana.json | 3 + src/plugins/es_ui_shared/kibana.json | 10 +- src/plugins/expressions/kibana.json | 3 +- src/plugins/inspector/kibana.json | 3 +- src/plugins/kibana_legacy/kibana.json | 3 +- src/plugins/kibana_utils/kibana.json | 7 +- src/plugins/maps_legacy/public/leaflet.js | 6 +- .../saved_objects_management/kibana.json | 3 +- src/plugins/telemetry/kibana.json | 3 + src/plugins/ui_actions/kibana.json | 5 +- .../public/app/components/main.tsx | 2 +- test/scripts/jenkins_build_kibana.sh | 9 +- test/scripts/jenkins_xpack_build_kibana.sh | 8 +- x-pack/plugins/alerts/kibana.json | 3 +- x-pack/plugins/apm/kibana.json | 3 + .../plugins/canvas/common/lib/autocomplete.ts | 2 +- x-pack/plugins/data_enhanced/kibana.json | 1 + x-pack/plugins/features/kibana.json | 3 +- x-pack/plugins/ingest_manager/kibana.json | 3 +- x-pack/plugins/lens/kibana.json | 3 +- x-pack/plugins/license_management/kibana.json | 3 +- x-pack/plugins/maps/kibana.json | 3 +- x-pack/plugins/spaces/kibana.json | 3 +- .../plugins/triggers_actions_ui/kibana.json | 3 +- .../dynamic_actions/dynamic_action_manager.ts | 5 +- 69 files changed, 904 insertions(+), 242 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md create mode 100644 packages/kbn-optimizer/src/common/bundle_refs.ts create mode 100644 packages/kbn-optimizer/src/worker/bundle_ref_module.ts create mode 100644 packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts create mode 100644 packages/kbn-optimizer/src/worker/entry_point_creator.ts create mode 100644 src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js diff --git a/.eslintrc.js b/.eslintrc.js index 9657719f0f526..8d5b4525d51ba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -472,6 +472,7 @@ module.exports = { { files: [ 'test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js', + 'src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js', '**/browser_exec_scripts/**/*.js', ], rules: { diff --git a/NOTICE.txt b/NOTICE.txt index 33c1d535d7df3..946b328b8766c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -21,6 +21,11 @@ used. Logarithmic ticks are places at powers of ten and at half those values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). For details, see https://github.com/flot/flot/pull/1328 +--- +This module was heavily inspired by the externals plugin that ships with webpack@97d58d31 +MIT License http://www.opensource.org/licenses/mit-license.php +Author Tobias Koppers @sokra + --- This product has relied on ASTExplorer that is licensed under MIT. diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md new file mode 100644 index 0000000000000..c46e60f2ecf6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [extraPublicDirs](./kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md) + +## PluginManifest.extraPublicDirs property + +> Warning: This API is now obsolete. +> +> + +Specifies directory names that can be imported by other ui-plugins built using the same instance of the @kbn/optimizer. A temporary measure we plan to replace with better mechanisms for sharing static code between plugins + +Signature: + +```typescript +readonly extraPublicDirs?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md index fe0ca476bbcb2..5edee51d6c523 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md @@ -21,6 +21,7 @@ Should never be used in code outside of Core but is exported for documentation p | Property | Type | Description | | --- | --- | --- | | [configPath](./kibana-plugin-core-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-core-server.configpath.md) used by the plugin, defaults to "id" in snake\_case format. | +| [extraPublicDirs](./kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md) | string[] | Specifies directory names that can be imported by other ui-plugins built using the same instance of the @kbn/optimizer. A temporary measure we plan to replace with better mechanisms for sharing static code between plugins | | [id](./kibana-plugin-core-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | | [kibanaVersion](./kibana-plugin-core-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | | [optionalPlugins](./kibana-plugin-core-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | diff --git a/examples/demo_search/kibana.json b/examples/demo_search/kibana.json index cdf74121ea2db..f909ca47fcd55 100644 --- a/examples/demo_search/kibana.json +++ b/examples/demo_search/kibana.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": ["data"], - "optionalPlugins": [] + "optionalPlugins": [], + "extraPublicDirs": ["common"] } diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 489f768552b28..b3ee0de096989 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": ["embeddable"], - "optionalPlugins": [] + "optionalPlugins": [], + "extraPublicDirs": ["public/todo", "public/hello_world"] } diff --git a/examples/url_generators_examples/kibana.json b/examples/url_generators_examples/kibana.json index cdb2127fdd26f..9658f5c7300aa 100644 --- a/examples/url_generators_examples/kibana.json +++ b/examples/url_generators_examples/kibana.json @@ -5,5 +5,8 @@ "server": false, "ui": true, "requiredPlugins": ["share"], - "optionalPlugins": [] + "optionalPlugins": [], + "extraPublicDirs": [ + "public/url_generator" + ] } diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md index c7f50c6af8dfd..9ff0f56344274 100644 --- a/packages/kbn-optimizer/README.md +++ b/packages/kbn-optimizer/README.md @@ -30,6 +30,18 @@ Bundles built by the the optimizer include a cache file which describes the info When a bundle is determined to be up-to-date a worker is not started for the bundle. If running the optimizer with the `--dev/--watch` flag, then all the files referenced by cached bundles are watched for changes. Once a change is detected in any of the files referenced by the built bundle a worker is started. If a file is changed that is referenced by several bundles then workers will be started for each bundle, combining workers together to respect the worker limit. +## Bundle Refs + +In order to dramatically reduce the size of our bundles, and the time it takes to build them, bundles will "ref" other bundles being built at the same time. When the optimizer starts it creates a list of "refs" that could be had from the list of bundles being built. Each worker uses that list to determine which import statements in a bundle should be replaced with a runtime reference to the output of another bundle. + +At runtime the bundles share a set of entry points via the `__kbnBundles__` global. By default a plugin shares `public` so that other code can use relative imports to access that directory. To expose additional directories they must be listed in the plugin's kibana.json "extraPublicDirs" field. The directories listed there will **also** be exported from the plugins bundle so that any other plugin can import that directory. "common" is commonly in the list of "extraPublicDirs". + +> NOTE: We plan to replace the `extraPublicDirs` functionality soon with better mechanisms for statically sharing code between bundles. + +When a directory is listed in the "extraPublicDirs" it will always be included in the bundle so that other plugins have access to it. The worker building the bundle has no way of knowing whether another plugin is using the directory, so be careful of adding test code or unnecessary directories to that list. + +Any import in a bundle which resolves into another bundles "context" directory, ie `src/plugins/*`, must map explicitly to a "public dir" exported by that plugin. If the resolved import is not in the list of public dirs an error will be thrown and the optimizer will fail to build that bundle until the error is fixed. + ## API To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index a372b9e394b9a..c7bf1dd60985d 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -45,6 +45,7 @@ "terser-webpack-plugin": "^2.1.2", "tinymath": "1.2.1", "url-loader": "^2.2.0", + "val-loader": "^1.1.1", "watchpack": "^1.6.0", "webpack": "^4.41.5", "webpack-merge": "^4.2.2" diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 7ddd10f4a388f..c881a15eac5b5 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -19,6 +19,6 @@ import './legacy/styles.scss'; import './index.scss'; -import { fooLibFn } from '../../foo/public/index'; +import { fooLibFn } from '../../foo/public'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index ec78a1bdf020e..b209bbca25ac4 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -23,7 +23,7 @@ jest.mock('fs'); const SPEC: BundleSpec = { contextDir: '/foo/bar', - entry: 'entry', + publicDirNames: ['public'], id: 'bar', outputDir: '/foo/bar/target', sourceRoot: '/foo', @@ -49,9 +49,11 @@ it('creates cache keys', () => { }, "spec": Object { "contextDir": "/foo/bar", - "entry": "entry", "id": "bar", "outputDir": "/foo/bar/target", + "publicDirNames": Array [ + "public", + ], "sourceRoot": "/foo", "type": "plugin", }, @@ -82,9 +84,11 @@ it('parses bundles from JSON specs', () => { "state": undefined, }, "contextDir": "/foo/bar", - "entry": "entry", "id": "bar", "outputDir": "/foo/bar/target", + "publicDirNames": Array [ + "public", + ], "sourceRoot": "/foo", "type": "plugin", }, diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index 9e2ad186ba40c..80af94c30f8da 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -29,8 +29,8 @@ export interface BundleSpec { readonly type: typeof VALID_BUNDLE_TYPES[0]; /** Unique id for this bundle */ readonly id: string; - /** Webpack entry request for this plugin, relative to the contextDir */ - readonly entry: string; + /** directory names relative to the contextDir that can be imported from */ + readonly publicDirNames: string[]; /** Absolute path to the plugin source directory */ readonly contextDir: string; /** Absolute path to the root of the repository */ @@ -44,8 +44,8 @@ export class Bundle { public readonly type: BundleSpec['type']; /** Unique identifier for this bundle */ public readonly id: BundleSpec['id']; - /** Path, relative to `contextDir`, to the entry file for the Webpack bundle */ - public readonly entry: BundleSpec['entry']; + /** directory names relative to the contextDir that can be imported from */ + public readonly publicDirNames: BundleSpec['publicDirNames']; /** * Absolute path to the root of the bundle context (plugin directory) * where the entry is resolved relative to and the default output paths @@ -62,7 +62,7 @@ export class Bundle { constructor(spec: BundleSpec) { this.type = spec.type; this.id = spec.id; - this.entry = spec.entry; + this.publicDirNames = spec.publicDirNames; this.contextDir = spec.contextDir; this.sourceRoot = spec.sourceRoot; this.outputDir = spec.outputDir; @@ -73,8 +73,6 @@ export class Bundle { /** * Calculate the cache key for this bundle based from current * mtime values. - * - * @param mtimes pre-fetched mtimes (ms || undefined) for all referenced files */ createCacheKey(files: string[], mtimes: Map): unknown { return { @@ -94,7 +92,7 @@ export class Bundle { return { type: this.type, id: this.id, - entry: this.entry, + publicDirNames: this.publicDirNames, contextDir: this.contextDir, sourceRoot: this.sourceRoot, outputDir: this.outputDir, @@ -134,9 +132,9 @@ export function parseBundles(json: string) { throw new Error('`bundles[]` must have a string `id` property'); } - const { entry } = spec; - if (!(typeof entry === 'string')) { - throw new Error('`bundles[]` must have a string `entry` property'); + const { publicDirNames } = spec; + if (!Array.isArray(publicDirNames) || !publicDirNames.every((d) => typeof d === 'string')) { + throw new Error('`bundles[]` must have an array of strings `publicDirNames` property'); } const { contextDir } = spec; @@ -157,7 +155,7 @@ export function parseBundles(json: string) { return new Bundle({ type, id, - entry, + publicDirNames, contextDir, sourceRoot, outputDir, diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 1dbc7f1d1b6b0..5ae3e4c28a201 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -25,6 +25,7 @@ export interface State { cacheKey?: unknown; moduleCount?: number; files?: string[]; + bundleRefExportIds?: string[]; } const DEFAULT_STATE: State = {}; @@ -87,6 +88,10 @@ export class BundleCache { return this.get().files; } + public getBundleRefExportIds() { + return this.get().bundleRefExportIds; + } + public getCacheKey() { return this.get().cacheKey; } diff --git a/packages/kbn-optimizer/src/common/bundle_refs.ts b/packages/kbn-optimizer/src/common/bundle_refs.ts new file mode 100644 index 0000000000000..a5c60f2031c0b --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle_refs.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import { Bundle } from './bundle'; +import { UnknownVals } from './ts_helpers'; + +export interface BundleRef { + bundleId: string; + contextDir: string; + contextPrefix: string; + entry: string; + exportId: string; +} + +export class BundleRefs { + static fromBundles(bundles: Bundle[]) { + return new BundleRefs( + bundles.reduce( + (acc: BundleRef[], b) => [ + ...acc, + ...b.publicDirNames.map( + (name): BundleRef => ({ + bundleId: b.id, + contextDir: b.contextDir, + // Path.resolve converts separators and strips the final separator + contextPrefix: Path.resolve(b.contextDir) + Path.sep, + entry: name, + exportId: `${b.type}/${b.id}/${name}`, + }) + ), + ], + [] + ) + ); + } + + static parseSpec(json: unknown) { + if (typeof json !== 'string') { + throw new Error('expected `bundleRefs` spec to be a JSON string'); + } + + let spec; + try { + spec = JSON.parse(json); + } catch (error) { + throw new Error('`bundleRefs` spec must be valid JSON'); + } + + if (!Array.isArray(spec)) { + throw new Error('`bundleRefs` spec must be an array'); + } + + return new BundleRefs( + spec.map( + (refSpec: UnknownVals): BundleRef => { + if (typeof refSpec !== 'object' || !refSpec) { + throw new Error('`bundleRefs[]` must be an object'); + } + + const { bundleId } = refSpec; + if (typeof bundleId !== 'string') { + throw new Error('`bundleRefs[].bundleId` must be a string'); + } + + const { contextDir } = refSpec; + if (typeof contextDir !== 'string' || !Path.isAbsolute(contextDir)) { + throw new Error('`bundleRefs[].contextDir` must be an absolute directory'); + } + + const { contextPrefix } = refSpec; + if (typeof contextPrefix !== 'string' || !Path.isAbsolute(contextPrefix)) { + throw new Error('`bundleRefs[].contextPrefix` must be an absolute directory'); + } + + const { entry } = refSpec; + if (typeof entry !== 'string') { + throw new Error('`bundleRefs[].entry` must be a string'); + } + + const { exportId } = refSpec; + if (typeof exportId !== 'string') { + throw new Error('`bundleRefs[].exportId` must be a string'); + } + + return { + bundleId, + contextDir, + contextPrefix, + entry, + exportId, + }; + } + ) + ); + } + + constructor(private readonly refs: BundleRef[]) {} + + public filterByExportIds(exportIds: string[]) { + return this.refs.filter((r) => exportIds.includes(r.exportId)); + } + + public filterByContextPrefix(bundle: Bundle, absolutePath: string) { + return this.refs.filter( + (ref) => ref.bundleId !== bundle.id && absolutePath.startsWith(ref.contextPrefix) + ); + } + + public toSpecJson() { + return JSON.stringify(this.refs); + } +} diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index c51905be04565..7d021a5ee7847 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -19,6 +19,7 @@ export * from './bundle'; export * from './bundle_cache'; +export * from './bundle_refs'; export * from './worker_config'; export * from './worker_messages'; export * from './compiler_messages'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 2814ab32017d2..2265bad9f6afa 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -9,9 +9,11 @@ OptimizerConfig { "state": undefined, }, "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, - "entry": "./public/index", "id": "bar", "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "publicDirNames": Array [ + "public", + ], "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, @@ -21,9 +23,11 @@ OptimizerConfig { "state": undefined, }, "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, - "entry": "./public/index", "id": "foo", "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "publicDirNames": Array [ + "public", + ], "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, @@ -35,16 +39,19 @@ OptimizerConfig { "plugins": Array [ Object { "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, + "extraPublicDirs": Array [], "id": "bar", "isUiPlugin": true, }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/baz, + "extraPublicDirs": Array [], "id": "baz", "isUiPlugin": false, }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, + "extraPublicDirs": Array [], "id": "foo", "isUiPlugin": true, }, @@ -55,8 +62,8 @@ OptimizerConfig { } `; -exports[`prepares assets for distribution: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { const foo = config.bundles.find((b) => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(5); + expect(foo.cache.getModuleCount()).toBe(6); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -148,7 +149,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { bar.cache.refresh(); expect(bar.cache.getModuleCount()).toBe( // code + styles + style/css-loader runtimes + public path updater - 21 + 18 ); expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` @@ -159,11 +160,8 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); diff --git a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts index 39064c64062e8..48cab508954a0 100644 --- a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts @@ -75,6 +75,7 @@ it('emits "bundle cached" event when everything is updated', async () => { optimizerCacheKey, files, moduleCount: files.length, + bundleRefExportIds: [], }); const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) @@ -115,6 +116,7 @@ it('emits "bundle not cached" event when cacheKey is up to date but caching is d optimizerCacheKey, files, moduleCount: files.length, + bundleRefExportIds: [], }); const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) @@ -155,6 +157,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is missing', async () optimizerCacheKey: undefined, files, moduleCount: files.length, + bundleRefExportIds: [], }); const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) @@ -195,6 +198,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes optimizerCacheKey: 'old', files, moduleCount: files.length, + bundleRefExportIds: [], }); const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) @@ -217,6 +221,53 @@ it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes `); }); +it('emits "bundle not cached" event when bundleRefExportIds is outdated, includes diff', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey, + files, + moduleCount: files.length, + bundleRefExportIds: ['plugin/bar/public'], + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "diff": "- Expected + + Received + +  [ + + \\"plugin/bar/public\\" +  ]", + "reason": "bundle references outdated", + "type": "bundle not cached", + }, + ] + `); +}); + it('emits "bundle not cached" event when cacheKey is missing', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, @@ -238,6 +289,7 @@ it('emits "bundle not cached" event when cacheKey is missing', async () => { optimizerCacheKey, files, moduleCount: files.length, + bundleRefExportIds: [], }); const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) @@ -276,6 +328,7 @@ it('emits "bundle not cached" event when cacheKey is outdated', async () => { optimizerCacheKey, files, moduleCount: files.length, + bundleRefExportIds: [], }); jest.spyOn(bundle, 'createCacheKey').mockImplementation(() => 'new'); diff --git a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts index 91d0f308e0ef6..176b17c979da9 100644 --- a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts @@ -29,14 +29,14 @@ jest.mock('fs'); jest.mock('watchpack'); const MockWatchPack: jest.MockedClass = jest.requireMock('watchpack'); -const bundleEntryPath = (bundle: Bundle) => `${bundle.contextDir}/${bundle.entry}`; +const bundleEntryPath = (bundle: Bundle) => `${bundle.contextDir}/public/index.ts`; const makeTestBundle = (id: string) => { const bundle = new Bundle({ type: 'plugin', id, contextDir: `/repo/plugins/${id}/public`, - entry: 'index.ts', + publicDirNames: ['public'], outputDir: `/repo/plugins/${id}/target/public`, sourceRoot: `/repo`, }); diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts index 4671276797049..ca50a49e26913 100644 --- a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts +++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts @@ -57,7 +57,7 @@ const assertReturnVal = (workers: Assignments[]) => { const testBundle = (id: string) => new Bundle({ contextDir: `/repo/plugin/${id}/public`, - entry: 'index.ts', + publicDirNames: ['public'], id, outputDir: `/repo/plugins/${id}/target/public`, sourceRoot: `/repo`, diff --git a/packages/kbn-optimizer/src/optimizer/bundle_cache.ts b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts index 55e8e1d3fd084..83db8570bd408 100644 --- a/packages/kbn-optimizer/src/optimizer/bundle_cache.ts +++ b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts @@ -20,7 +20,7 @@ import * as Rx from 'rxjs'; import { mergeAll } from 'rxjs/operators'; -import { Bundle } from '../common'; +import { Bundle, BundleRefs } from '../common'; import { OptimizerConfig } from './optimizer_config'; import { getMtimes } from './get_mtimes'; @@ -35,7 +35,9 @@ export interface BundleNotCachedEvent { | 'optimizer cache key mismatch' | 'missing cache key' | 'cache key mismatch' - | 'cache disabled'; + | 'cache disabled' + | 'bundle references missing' + | 'bundle references outdated'; diff?: string; bundle: Bundle; } @@ -52,6 +54,7 @@ export function getBundleCacheEvent$( return Rx.defer(async () => { const events: BundleCacheEvent[] = []; const eligibleBundles: Bundle[] = []; + const bundleRefs = BundleRefs.fromBundles(config.bundles); for (const bundle of config.bundles) { if (!config.cache) { @@ -93,6 +96,32 @@ export function getBundleCacheEvent$( continue; } + const bundleRefExportIds = bundle.cache.getBundleRefExportIds(); + if (!bundleRefExportIds) { + events.push({ + type: 'bundle not cached', + reason: 'bundle references missing', + bundle, + }); + continue; + } + + const refs = bundleRefs.filterByExportIds(bundleRefExportIds); + + const bundleRefsDiff = diffCacheKey( + refs.map((r) => r.exportId).sort((a, b) => a.localeCompare(b)), + bundleRefExportIds + ); + if (bundleRefsDiff) { + events.push({ + type: 'bundle not cached', + reason: 'bundle references outdated', + diff: bundleRefsDiff, + bundle, + }); + continue; + } + eligibleBundles.push(bundle); } diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.ts index 2766f6d63702b..d0aaad979485d 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.ts @@ -37,16 +37,6 @@ import { OptimizerConfig } from './optimizer_config'; const OPTIMIZER_DIR = Path.dirname(require.resolve('../../package.json')); const RELATIVE_DIR = Path.relative(REPO_ROOT, OPTIMIZER_DIR); -function omit(obj: T, keys: K[]): Omit { - const result: any = {}; - for (const [key, value] of Object.entries(obj) as any) { - if (!keys.includes(key)) { - result[key] = value; - } - } - return result as Omit; -} - export function diffCacheKey(expected?: unknown, actual?: unknown) { const expectedJson = jsonStable(expected, { space: ' ', @@ -185,7 +175,7 @@ export async function getOptimizerCacheKey(config: OptimizerConfig) { bootstrap, deletedPaths, modifiedTimes: {} as Record, - workerConfig: omit(config.getWorkerConfig('♻'), ['watch', 'profileWebpack', 'cache']), + workerConfig: config.getCacheableWorkerConfig(), }; const mtimes = await getMtimes(modifiedPaths); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index 2174c488ad6cc..bbd3ddc11f448 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -31,16 +31,19 @@ it('returns a bundle for core and each plugin', () => { directory: '/repo/plugins/foo', id: 'foo', isUiPlugin: true, + extraPublicDirs: [], }, { directory: '/repo/plugins/bar', id: 'bar', isUiPlugin: false, + extraPublicDirs: [], }, { directory: '/outside/of/repo/plugins/baz', id: 'baz', isUiPlugin: true, + extraPublicDirs: [], }, ], '/repo' @@ -49,17 +52,21 @@ it('returns a bundle for core and each plugin', () => { Array [ Object { "contextDir": /plugins/foo, - "entry": "./public/index", "id": "foo", "outputDir": /plugins/foo/target/public, + "publicDirNames": Array [ + "public", + ], "sourceRoot": , "type": "plugin", }, Object { "contextDir": "/outside/of/repo/plugins/baz", - "entry": "./public/index", "id": "baz", "outputDir": "/outside/of/repo/plugins/baz/target/public", + "publicDirNames": Array [ + "public", + ], "sourceRoot": , "type": "plugin", }, diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index b75a8a6edc264..2635289088725 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -31,7 +31,7 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri new Bundle({ type: 'plugin', id: p.id, - entry: './public/index', + publicDirNames: ['public', ...p.extraPublicDirs], sourceRoot: repoRoot, contextDir: p.directory, outputDir: Path.resolve(p.directory, 'target/public'), diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts index e047b6d1e44cf..0961881df461c 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts @@ -37,21 +37,25 @@ it('parses kibana.json files of plugins found in pluginDirs', () => { Array [ Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar, + "extraPublicDirs": Array [], "id": "bar", "isUiPlugin": true, }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz, + "extraPublicDirs": Array [], "id": "baz", "isUiPlugin": false, }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo, + "extraPublicDirs": Array [], "id": "foo", "isUiPlugin": true, }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz, + "extraPublicDirs": Array [], "id": "test_baz", "isUiPlugin": false, }, diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index 992feab6cd364..bfc60a29efa27 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -26,6 +26,7 @@ export interface KibanaPlatformPlugin { readonly directory: string; readonly id: string; readonly isUiPlugin: boolean; + readonly extraPublicDirs: string[]; } /** @@ -64,9 +65,24 @@ function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { throw new TypeError('expected new platform plugin manifest to have a string id'); } + let extraPublicDirs: string[] | undefined; + if (manifest.extraPublicDirs) { + if ( + !Array.isArray(manifest.extraPublicDirs) || + !manifest.extraPublicDirs.every((p) => typeof p === 'string') + ) { + throw new TypeError( + 'expected new platform plugin manifest to have an array of strings `extraPublicDirs` property' + ); + } + + extraPublicDirs = manifest.extraPublicDirs as string[]; + } + return { directory: Path.dirname(manifestPath), id: manifest.id, isUiPlugin: !!manifest.ui, + extraPublicDirs: extraPublicDirs || [], }; } diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index c929cf62d1bb0..4527052fa821a 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -24,7 +24,7 @@ import execa from 'execa'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle } from '../common'; +import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle, BundleRefs } from '../common'; import { OptimizerConfig } from './optimizer_config'; @@ -74,7 +74,11 @@ function usingWorkerProc( ) { return Rx.using( (): ProcResource => { - const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map((b) => b.toSpec()))]; + const args = [ + JSON.stringify(workerConfig), + JSON.stringify(bundles.map((b) => b.toSpec())), + BundleRefs.fromBundles(config.bundles).toSpecJson(), + ]; const proc = execa.node(require.resolve('../worker/run_worker'), args, { nodeOptions: [ diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 37d8a4f5eb8ae..c9e9b3ad01ccc 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -20,7 +20,7 @@ import Path from 'path'; import Os from 'os'; -import { Bundle, WorkerConfig } from '../common'; +import { Bundle, WorkerConfig, CacheableWorkerConfig } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; @@ -34,6 +34,16 @@ function pickMaxWorkerCount(dist: boolean) { return Math.max(maxWorkers, 2); } +function omit(obj: T, keys: K[]): Omit { + const result: any = {}; + for (const [key, value] of Object.entries(obj) as any) { + if (!keys.includes(key)) { + result[key] = value; + } + } + return result as Omit; +} + interface Options { /** absolute path to root of the repo/build */ repoRoot: string; @@ -152,7 +162,7 @@ export class OptimizerConfig { new Bundle({ type: 'entry', id: 'core', - entry: './public/index', + publicDirNames: ['public', 'public/utils'], sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), @@ -198,4 +208,14 @@ export class OptimizerConfig { browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev', }; } + + getCacheableWorkerConfig(): CacheableWorkerConfig { + return omit(this.getWorkerConfig('♻'), [ + // these config options don't change the output of the bundles, so + // should not invalidate caches when they change + 'watch', + 'profileWebpack', + 'cache', + ]); + } } diff --git a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts new file mode 100644 index 0000000000000..cde25564cf528 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts @@ -0,0 +1,67 @@ +/* eslint-disable @kbn/eslint/require-license-header */ + +/** + * @notice + * + * This module was heavily inspired by the externals plugin that ships with webpack@97d58d31 + * MIT License http://www.opensource.org/licenses/mit-license.php + * Author Tobias Koppers @sokra + */ + +// @ts-ignore not typed by @types/webpack +import Module from 'webpack/lib/Module'; + +export class BundleRefModule extends Module { + public built = false; + public buildMeta?: any; + public buildInfo?: any; + public exportsArgument = '__webpack_exports__'; + + constructor(public readonly exportId: string) { + super('kbn/bundleRef', null); + } + + libIdent() { + return this.exportId; + } + + chunkCondition(chunk: any) { + return chunk.hasEntryModule(); + } + + identifier() { + return '@kbn/bundleRef ' + JSON.stringify(this.exportId); + } + + readableIdentifier() { + return this.identifier(); + } + + needRebuild() { + return false; + } + + build(_: any, __: any, ___: any, ____: any, callback: () => void) { + this.built = true; + this.buildMeta = {}; + this.buildInfo = {}; + callback(); + } + + source() { + return ` + __webpack_require__.r(__webpack_exports__); + var ns = __kbnBundles__.get('${this.exportId}'); + Object.defineProperties(__webpack_exports__, Object.getOwnPropertyDescriptors(ns)) + `; + } + + size() { + return 42; + } + + updateHash(hash: any) { + hash.update(this.identifier()); + super.updateHash(hash); + } +} diff --git a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts new file mode 100644 index 0000000000000..6defcaa787b7d --- /dev/null +++ b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts @@ -0,0 +1,165 @@ +/* eslint-disable @kbn/eslint/require-license-header */ + +/** + * @notice + * + * This module was heavily inspired by the externals plugin that ships with webpack@97d58d31 + * MIT License http://www.opensource.org/licenses/mit-license.php + * Author Tobias Koppers @sokra + */ + +import Path from 'path'; +import Fs from 'fs'; + +import webpack from 'webpack'; + +import { Bundle, BundleRefs, BundleRef } from '../common'; +import { BundleRefModule } from './bundle_ref_module'; + +const RESOLVE_EXTENSIONS = ['.js', '.ts', '.tsx']; + +function safeStat(path: string): Promise { + return new Promise((resolve, reject) => { + Fs.stat(path, (error, stat) => { + if (error?.code === 'ENOENT') { + resolve(undefined); + } else if (error) { + reject(error); + } else { + resolve(stat); + } + }); + }); +} + +interface RequestData { + context: string; + dependencies: Array<{ request: string }>; +} + +type Callback = (error?: any, result?: T) => void; +type ModuleFactory = (data: RequestData, callback: Callback) => void; + +/** + * Isolate the weired type juggling we have to do to add a hook to the webpack compiler + */ +function hookIntoCompiler( + compiler: webpack.Compiler, + handler: (context: string, request: string) => Promise +) { + compiler.hooks.compile.tap('BundleRefsPlugin', (compilationParams: any) => { + compilationParams.normalModuleFactory.hooks.factory.tap( + 'BundleRefsPlugin/normalModuleFactory/factory', + (wrappedFactory: ModuleFactory): ModuleFactory => (data, callback) => { + const context = data.context; + const dep = data.dependencies[0]; + + handler(context, dep.request).then( + (result) => { + if (!result) { + wrappedFactory(data, callback); + } else { + callback(undefined, result); + } + }, + (error) => callback(error) + ); + } + ); + }); +} + +export class BundleRefsPlugin { + private resolvedRequestCache = new Map>(); + + constructor(private readonly bundle: Bundle, public readonly bundleRefs: BundleRefs) {} + + apply(compiler: webpack.Compiler) { + hookIntoCompiler(compiler, async (context, request) => { + const ref = await this.resolveRef(context, request); + if (ref) { + return new BundleRefModule(ref.exportId); + } + }); + } + + private cachedResolveRequest(context: string, request: string) { + const absoluteRequest = Path.resolve(context, request); + const cached = this.resolvedRequestCache.get(absoluteRequest); + + if (cached) { + return cached; + } + + const promise = this.resolveRequest(absoluteRequest); + this.resolvedRequestCache.set(absoluteRequest, promise); + return promise; + } + + private async resolveRequest(absoluteRequest: string) { + const stats = await safeStat(absoluteRequest); + if (stats && stats.isFile()) { + return absoluteRequest; + } + + if (stats?.isDirectory()) { + for (const ext of RESOLVE_EXTENSIONS) { + const indexPath = Path.resolve(absoluteRequest, `index${ext}`); + const indexStats = await safeStat(indexPath); + if (indexStats?.isFile()) { + return indexPath; + } + } + } + + return; + } + + /** + * Determine if an import request resolves to a bundleRef export id. If the + * request resolves to a bundle ref context but none of the exported directories + * then an error is thrown. If the request does not resolve to a bundleRef then + * undefined is returned. Otherwise it returns the referenced bundleRef. + */ + private async resolveRef(context: string, request: string) { + // ignore imports that have loaders defined or are not relative seeming + if (request.includes('!') || !request.startsWith('.')) { + return; + } + + const requestExt = Path.extname(request); + if (requestExt && !RESOLVE_EXTENSIONS.includes(requestExt)) { + return; + } + + const resolved = await this.cachedResolveRequest(context, request); + if (!resolved) { + return; + } + + const eligibleRefs = this.bundleRefs.filterByContextPrefix(this.bundle, resolved); + if (!eligibleRefs.length) { + // import doesn't match a bundle context + return; + } + + let matchingRef: BundleRef | undefined; + for (const ref of eligibleRefs) { + const resolvedEntry = await this.cachedResolveRequest(ref.contextDir, ref.entry); + if (resolved === resolvedEntry) { + matchingRef = ref; + break; + } + } + + if (!matchingRef) { + const bundleId = Array.from(new Set(eligibleRefs.map((r) => r.bundleId))).join(', '); + const publicDir = eligibleRefs.map((r) => r.entry).join(', '); + throw new Error( + `import [${request}] references a non-public export of the [${bundleId}] bundle and must point to one of the public directories: [${publicDir}]` + ); + } + + return matchingRef; + } +} diff --git a/packages/kbn-optimizer/src/worker/entry_point_creator.ts b/packages/kbn-optimizer/src/worker/entry_point_creator.ts new file mode 100644 index 0000000000000..a613e3e8925a4 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/entry_point_creator.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = function ({ entries }: { entries: Array<{ importId: string; relPath: string }> }) { + const lines = entries.map(({ importId, relPath }) => [ + `__kbnBundles__.define('${importId}', __webpack_require__, require.resolve('./${relPath}'))`, + ]); + + return { + code: lines.join('\n'), + }; +}; diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 4ab289d031d72..de5e9372e9e7a 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -35,7 +35,9 @@ import { WorkerConfig, ascending, parseFilePath, + BundleRefs, } from '../common'; +import { BundleRefModule } from './bundle_ref_module'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; import { @@ -43,7 +45,6 @@ import { isNormalModule, isIgnoredModule, isConcatenatedModule, - WebpackNormalModule, getModulePath, } from './webpack_helpers'; @@ -98,40 +99,43 @@ const observeCompiler = ( }); } - const normalModules = stats.compilation.modules.filter( - (module): module is WebpackNormalModule => { - if (isNormalModule(module)) { - return true; - } + const bundleRefExportIds: string[] = []; + const referencedFiles = new Set(); + let normalModuleCount = 0; + + for (const module of stats.compilation.modules) { + if (isNormalModule(module)) { + normalModuleCount += 1; + const path = getModulePath(module); + const parsedPath = parseFilePath(path); - if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { - return false; + if (!parsedPath.dirs.includes('node_modules')) { + referencedFiles.add(path); + continue; } - throw new Error(`Unexpected module type: ${inspect(module)}`); + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + continue; } - ); - - const referencedFiles = new Set(); - for (const module of normalModules) { - const path = getModulePath(module); - const parsedPath = parseFilePath(path); + if (module instanceof BundleRefModule) { + bundleRefExportIds.push(module.exportId); + continue; + } - if (!parsedPath.dirs.includes('node_modules')) { - referencedFiles.add(path); + if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { continue; } - const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); - const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); - referencedFiles.add( - Path.join( - parsedPath.root, - ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), - 'package.json' - ) - ); + throw new Error(`Unexpected module type: ${inspect(module)}`); } const files = Array.from(referencedFiles).sort(ascending((p) => p)); @@ -150,14 +154,15 @@ const observeCompiler = ( ); bundle.cache.set({ + bundleRefExportIds, optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount: normalModules.length, + moduleCount: normalModuleCount, files, }); return compilerMsgs.compilerSuccess({ - moduleCount: normalModules.length, + moduleCount: normalModuleCount, }); }) ); @@ -185,8 +190,14 @@ const observeCompiler = ( /** * Run webpack compilers */ -export const runCompilers = (workerConfig: WorkerConfig, bundles: Bundle[]) => { - const multiCompiler = webpack(bundles.map((def) => getWebpackConfig(def, workerConfig))); +export const runCompilers = ( + workerConfig: WorkerConfig, + bundles: Bundle[], + bundleRefs: BundleRefs +) => { + const multiCompiler = webpack( + bundles.map((def) => getWebpackConfig(def, bundleRefs, workerConfig)) + ); return Rx.merge( /** diff --git a/packages/kbn-optimizer/src/worker/run_worker.ts b/packages/kbn-optimizer/src/worker/run_worker.ts index f83c69477f471..178637d39ab00 100644 --- a/packages/kbn-optimizer/src/worker/run_worker.ts +++ b/packages/kbn-optimizer/src/worker/run_worker.ts @@ -19,7 +19,14 @@ import * as Rx from 'rxjs'; -import { parseBundles, parseWorkerConfig, WorkerMsg, isWorkerMsg, WorkerMsgs } from '../common'; +import { + parseBundles, + parseWorkerConfig, + WorkerMsg, + isWorkerMsg, + WorkerMsgs, + BundleRefs, +} from '../common'; import { runCompilers } from './run_compilers'; @@ -76,11 +83,12 @@ setInterval(() => { Rx.defer(() => { const workerConfig = parseWorkerConfig(process.argv[2]); const bundles = parseBundles(process.argv[3]); + const bundleRefs = BundleRefs.parseSpec(process.argv[4]); // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; - return runCompilers(workerConfig, bundles); + return runCompilers(workerConfig, bundles, bundleRefs); }).subscribe( (msg) => { send(msg); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index d31c098ca1f2e..3daf21cdc38cc 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -17,10 +17,8 @@ * under the License. */ -import Fs from 'fs'; import Path from 'path'; -import normalizePath from 'normalize-path'; import { stringifyRequest } from 'loader-utils'; import webpack from 'webpack'; // @ts-ignore @@ -32,88 +30,22 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; +import { Bundle, BundleRefs, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; +import { BundleRefsPlugin } from './bundle_refs_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); -const SHARED_BUNDLES = [ - { - type: 'entry', - id: 'core', - rootRelativeDir: 'src/core/public', - }, - { - type: 'plugin', - id: 'data', - rootRelativeDir: 'src/plugins/data/public', - }, - { - type: 'plugin', - id: 'kibanaReact', - rootRelativeDir: 'src/plugins/kibana_react/public', - }, - { - type: 'plugin', - id: 'kibanaUtils', - rootRelativeDir: 'src/plugins/kibana_utils/public', - }, - { - type: 'plugin', - id: 'esUiShared', - rootRelativeDir: 'src/plugins/es_ui_shared/public', - }, -]; - -/** - * Determine externals statements for require/import statements by looking - * for requests resolving to the primary public export of the data, kibanaReact, - * amd kibanaUtils plugins. If this module is being imported then rewrite - * the import to access the global `__kbnBundles__` variables and access - * the relavent properties from that global object. - * - * @param bundle - * @param context the directory containing the module which made `request` - * @param request the request for a module from a commonjs require() call or import statement - */ -function dynamicExternals(bundle: Bundle, context: string, request: string) { - // ignore imports that have loaders defined or are not relative seeming - if (request.includes('!') || !request.startsWith('.')) { - return; - } - - // determine the most acurate resolution string we can without running full resolution - const rootRelative = normalizePath( - Path.relative(bundle.sourceRoot, Path.resolve(context, request)) - ); - for (const sharedBundle of SHARED_BUNDLES) { - if ( - rootRelative !== sharedBundle.rootRelativeDir || - `${bundle.type}/${bundle.id}` === `${sharedBundle.type}/${sharedBundle.id}` - ) { - continue; - } - - return `__kbnBundles__['${sharedBundle.type}/${sharedBundle.id}']`; - } - - // import doesn't match a root public import - return undefined; -} - -export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { - const extensions = ['.js', '.ts', '.tsx', '.json']; - const entryExtension = extensions.find((ext) => - Fs.existsSync(Path.resolve(bundle.contextDir, bundle.entry) + ext) - ); +export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: WorkerConfig) { + const ENTRY_CREATOR = require.resolve('./entry_point_creator'); const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, context: bundle.contextDir, cache: true, entry: { - [bundle.id]: `${bundle.entry}${entryExtension}`, + [bundle.id]: ENTRY_CREATOR, }, devtool: worker.dist ? false : '#cheap-source-map', @@ -128,27 +60,19 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { info.absoluteResourcePath )}${info.query}`, jsonpFunction: `${bundle.id}_bundle_jsonpfunction`, - // When the entry point is loaded, assign it's default export - // to a key on the global `__kbnBundles__` object. - library: ['__kbnBundles__', `${bundle.type}/${bundle.id}`], }, optimization: { noEmitOnErrors: true, }, - externals: [ - UiSharedDeps.externals, - function (context, request, cb) { - try { - cb(undefined, dynamicExternals(bundle, context, request)); - } catch (error) { - cb(error, undefined); - } - }, - ], + externals: [UiSharedDeps.externals], - plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], + plugins: [ + new CleanWebpackPlugin(), + new DisallowedSyntaxPlugin(), + new BundleRefsPlugin(bundle, bundleRefs), + ], module: { // no parse rules for a few known large packages which have no require() statements @@ -162,11 +86,28 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { rules: [ { - include: [`${Path.resolve(bundle.contextDir, bundle.entry)}${entryExtension}`], - loader: UiSharedDeps.publicPathLoader, - options: { - key: bundle.id, - }, + include: [ENTRY_CREATOR], + use: [ + { + loader: UiSharedDeps.publicPathLoader, + options: { + key: bundle.id, + }, + }, + { + loader: require.resolve('val-loader'), + options: { + entries: bundle.publicDirNames.map((name) => { + const absolute = Path.resolve(bundle.contextDir, name); + const newContext = Path.dirname(ENTRY_CREATOR); + return { + importId: `${bundle.type}/${bundle.id}/${name}`, + relPath: Path.relative(newContext, absolute), + }; + }), + }, + }, + ], }, { test: /\.css$/, @@ -310,7 +251,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, resolve: { - extensions, + extensions: ['.js', '.ts', '.tsx', 'json'], mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/src/core/public/plugins/plugin_reader.test.ts b/src/core/public/plugins/plugin_reader.test.ts index d4324f81de8e6..b3bc84f30daac 100644 --- a/src/core/public/plugins/plugin_reader.test.ts +++ b/src/core/public/plugins/plugin_reader.test.ts @@ -17,25 +17,37 @@ * under the License. */ -import { CoreWindow, read, UnknownPluginInitializer } from './plugin_reader'; +import { CoreWindow, read } from './plugin_reader'; + +const coreWindow: CoreWindow & { + __kbnBundles__: { stub(key: string, value: any): void }; +} = window as any; -const coreWindow: CoreWindow = window as any; beforeEach(() => { - coreWindow.__kbnBundles__ = {}; + const stubs = new Map(); + coreWindow.__kbnBundles__ = { + get(key) { + return stubs.get(key); + }, + has(key) { + return stubs.has(key); + }, + stub(key, value) { + stubs.set(key, value); + }, + }; }); it('handles undefined plugin exports', () => { - coreWindow.__kbnBundles__['plugin/foo'] = undefined; - expect(() => { read('foo'); }).toThrowError(`Definition of plugin "foo" not found and may have failed to load.`); }); it('handles plugin exports with a "plugin" export that is not a function', () => { - coreWindow.__kbnBundles__['plugin/foo'] = { + coreWindow.__kbnBundles__.stub('plugin/foo/public', { plugin: 1234, - } as any; + }); expect(() => { read('foo'); @@ -43,11 +55,8 @@ it('handles plugin exports with a "plugin" export that is not a function', () => }); it('returns the plugin initializer when the "plugin" named export is a function', () => { - const plugin: UnknownPluginInitializer = () => { - return undefined as any; - }; - - coreWindow.__kbnBundles__['plugin/foo'] = { plugin }; + const plugin = () => {}; + coreWindow.__kbnBundles__.stub('plugin/foo/public', { plugin }); expect(read('foo')).toBe(plugin); }); diff --git a/src/core/public/plugins/plugin_reader.ts b/src/core/public/plugins/plugin_reader.ts index 1907dfa6a3e99..d80bda7483775 100644 --- a/src/core/public/plugins/plugin_reader.ts +++ b/src/core/public/plugins/plugin_reader.ts @@ -31,7 +31,8 @@ export type UnknownPluginInitializer = PluginInitializer { optionalPlugins: true, ui: true, server: true, + extraPublicDirs: true, }; return new Set(Object.keys(manifestFields)); @@ -70,7 +71,11 @@ const KNOWN_MANIFEST_FIELDS = (() => { * @param packageInfo Kibana package info. * @internal */ -export async function parseManifest(pluginPath: string, packageInfo: PackageInfo, log: Logger) { +export async function parseManifest( + pluginPath: string, + packageInfo: PackageInfo, + log: Logger +): Promise { const manifestPath = resolve(pluginPath, MANIFEST_FILE_NAME); let manifestContent; @@ -130,6 +135,19 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo ); } + if ( + manifest.extraPublicDirs && + (!Array.isArray(manifest.extraPublicDirs) || + !manifest.extraPublicDirs.every((dir) => typeof dir === 'string')) + ) { + throw PluginDiscoveryError.invalidManifest( + manifestPath, + new Error( + `The "extraPublicDirs" in plugin manifest for "${manifest.id}" should be an array of strings.` + ) + ); + } + const expectedKibanaVersion = typeof manifest.kibanaVersion === 'string' && manifest.kibanaVersion ? manifest.kibanaVersion @@ -175,6 +193,7 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [], ui: includesUiPlugin, server: includesServerPlugin, + extraPublicDirs: manifest.extraPublicDirs, }; } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 4fa4e1780e596..2ca5c9f6ed3c5 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -153,6 +153,14 @@ export interface PluginManifest { * Specifies whether plugin includes some server-side specific functionality. */ readonly server: boolean; + + /** + * Specifies directory names that can be imported by other ui-plugins built + * using the same instance of the @kbn/optimizer. A temporary measure we plan + * to replace with better mechanisms for sharing static code between plugins + * @deprecated + */ + readonly extraPublicDirs?: string[]; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 78cc02d39e6c4..ecfa09fbd37f3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1520,6 +1520,8 @@ export interface PluginInitializerContext { // @public export interface PluginManifest { readonly configPath: ConfigPath; + // @deprecated + readonly extraPublicDirs?: string[]; readonly id: PluginName; readonly kibanaVersion: string; readonly optionalPlugins: readonly PluginName[]; @@ -2549,8 +2551,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:232:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:238:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:238:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:240:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/legacy/ui/ui_render/bootstrap/app_bootstrap.js b/src/legacy/ui/ui_render/bootstrap/app_bootstrap.js index 0e6936dd64a15..19f75317883d7 100644 --- a/src/legacy/ui/ui_render/bootstrap/app_bootstrap.js +++ b/src/legacy/ui/ui_render/bootstrap/app_bootstrap.js @@ -22,9 +22,11 @@ import { createHash } from 'crypto'; import { readFile } from 'fs'; import { resolve } from 'path'; +import { kbnBundlesLoaderSource } from './kbn_bundles_loader_source'; + export class AppBootstrap { constructor({ templateData }) { - this.templateData = templateData; + this.templateData = { ...templateData, kbnBundlesLoaderSource }; this._rawTemplate = undefined; } diff --git a/src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js b/src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js new file mode 100644 index 0000000000000..cb5488118cc77 --- /dev/null +++ b/src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + kbnBundlesLoaderSource: `(${kbnBundlesLoader.toString()})();`, +}; + +function kbnBundlesLoader() { + var modules = {}; + + function has(prop) { + return Object.prototype.hasOwnProperty.call(modules, prop); + } + + function define(key, bundleRequire, bundleModuleKey) { + if (has(key)) { + throw new Error('__kbnBundles__ already has a module defined for "' + key + '"'); + } + + modules[key] = { + bundleRequire, + bundleModuleKey, + }; + } + + function get(key) { + if (!has(key)) { + throw new Error('__kbnBundles__ does not have a module defined for "' + key + '"'); + } + + return modules[key].bundleRequire(modules[key].bundleModuleKey); + } + + return { has: has, define: define, get: get }; +} diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index e8f05b46f7061..ca2e944489a73 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -3,6 +3,7 @@ window.__kbnStrictCsp__ = kbnCsp.strictCsp; window.__kbnDarkMode__ = {{darkMode}}; window.__kbnThemeVersion__ = "{{themeVersion}}"; window.__kbnPublicPath__ = {{publicPathMap}}; +window.__kbnBundles__ = {{kbnBundlesLoaderSource}} if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -78,12 +79,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { {{/each}} ], function () { {{#unless legacyBundlePath}} - if (!__kbnBundles__ || !__kbnBundles__['entry/core'] || typeof __kbnBundles__['entry/core'].__kbnBootstrap__ !== 'function') { - console.error('entry/core bundle did not load correctly'); - failure(); - } else { - __kbnBundles__['entry/core'].__kbnBootstrap__() - } + __kbnBundles__.get('entry/core/public').__kbnBootstrap__(); {{/unless}} load([ diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index b09d4861b343b..673e19155879a 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -26,8 +26,6 @@ import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; -const uniq = (...items) => Array.from(new Set(items)); - /** * @typedef {import('../../server/kbn_server').default} KbnServer * @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit @@ -150,15 +148,7 @@ export function uiRenderMixin(kbnServer, server, config) { ]), ]; - const kpPluginIds = uniq( - // load these plugins first, they are "shared" and other bundles access their - // public/index exports without considering topographic sorting by plugin deps (for now) - 'kibanaUtils', - 'kibanaReact', - 'data', - 'esUiShared', - ...kbnServer.newPlatform.__internals.uiPlugins.public.keys() - ); + const kpPluginIds = Array.from(kbnServer.newPlatform.__internals.uiPlugins.public.keys()); const jsDependencyPaths = [ ...UiSharedDeps.jsDepFilenames.map( diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index f5df747f17e1e..3e5d96a4bc47b 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -7,5 +7,6 @@ "expressions", "uiActions" ], - "optionalPlugins": ["usageCollection"] + "optionalPlugins": ["usageCollection"], + "extraPublicDirs": ["common", "common/utils/abort_utils"] } diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 535527b4d09db..06b0e88da334f 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -6,5 +6,8 @@ "requiredPlugins": [ "inspector", "uiActions" + ], + "extraPublicDirs": [ + "public/lib/test_samples" ] } diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index 5f772b3c83da9..980f43ea46a68 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -2,5 +2,13 @@ "id": "esUiShared", "version": "kibana", "ui": true, - "server": true + "server": true, + "extraPublicDirs": [ + "static/ace_x_json/hooks", + "static/validators/string", + "static/forms/hook_form_lib", + "static/forms/helpers", + "static/forms/components", + "static/forms/helpers/field_validators/types" + ] } diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 5d2112103e94d..4774c69cc29ff 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -5,5 +5,6 @@ "ui": true, "requiredPlugins": [ "bfetch" - ] + ], + "extraPublicDirs": ["common", "common/fonts"] } diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json index 39d3ff65eed53..99a38d2928df6 100644 --- a/src/plugins/inspector/kibana.json +++ b/src/plugins/inspector/kibana.json @@ -2,5 +2,6 @@ "id": "inspector", "version": "kibana", "server": false, - "ui": true + "ui": true, + "extraPublicDirs": ["common", "common/adapters/request"] } diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index e96b4859a36d0..606acd8b88b05 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -2,5 +2,6 @@ "id": "kibanaLegacy", "version": "kibana", "server": true, - "ui": true + "ui": true, + "extraPublicDirs": ["common/kbn_base_url"] } diff --git a/src/plugins/kibana_utils/kibana.json b/src/plugins/kibana_utils/kibana.json index 6fa39d82d1021..7e2127c27548e 100644 --- a/src/plugins/kibana_utils/kibana.json +++ b/src/plugins/kibana_utils/kibana.json @@ -1,5 +1,10 @@ { "id": "kibanaUtils", "version": "kibana", - "ui": true + "ui": true, + "extraPublicDirs": [ + "common", + "demos/state_containers/todomvc", + "common/state_containers" + ] } diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js index e36da2c52b8c5..bee75021c76ad 100644 --- a/src/plugins/maps_legacy/public/leaflet.js +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -17,8 +17,6 @@ * under the License. */ -export let L; - if (!window.hasOwnProperty('L')) { require('leaflet/dist/leaflet.css'); window.L = require('leaflet/dist/leaflet.js'); @@ -31,6 +29,6 @@ if (!window.hasOwnProperty('L')) { require('leaflet-draw/dist/leaflet.draw.js'); require('leaflet-responsive-popup/leaflet.responsive.popup.css'); require('leaflet-responsive-popup/leaflet.responsive.popup.js'); -} else { - L = window.L; } + +export const L = window.L; diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json index 22135ce4558ae..6184d890c415c 100644 --- a/src/plugins/saved_objects_management/kibana.json +++ b/src/plugins/saved_objects_management/kibana.json @@ -4,5 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["home", "management", "data"], - "optionalPlugins": ["dashboard", "visualizations", "discover"] + "optionalPlugins": ["dashboard", "visualizations", "discover"], + "extraPublicDirs": ["public/lib"] } diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index f623f4f2a565d..a497597762520 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -6,5 +6,8 @@ "requiredPlugins": [ "telemetryCollectionManager", "usageCollection" + ], + "extraPublicDirs": [ + "common/constants" ] } diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index 44ecbbfa68408..907cbabbdf9c9 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -2,5 +2,8 @@ "id": "uiActions", "version": "kibana", "server": false, - "ui": true + "ui": true, + "extraPublicDirs": [ + "public/tests/test_samples" + ] } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx index ace2af2b4f0cf..b4f9634b23d29 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; import { first } from 'rxjs/operators'; import { IInterpreterRenderHandlers, ExpressionValue } from 'src/plugins/expressions'; -import { RequestAdapter, DataAdapter } from '../../../../../../../src/plugins/inspector'; +import { RequestAdapter, DataAdapter } from '../../../../../../../src/plugins/inspector/public'; import { Adapters, ExpressionRenderHandler } from '../../types'; import { getExpressions } from '../../services'; diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index e3f46e7a6ada4..3e49edc8e6ae5 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,9 +2,16 @@ source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" +echo " -> building examples separate from test plugins" node scripts/build_kibana_platform_plugins \ --oss \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --no-examples \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ --verbose; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index c962b962b1e5e..58ef6a42d3fe4 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,8 +3,14 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" +echo " -> building examples separate from test plugins" node scripts/build_kibana_platform_plugins \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --no-examples \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ diff --git a/x-pack/plugins/alerts/kibana.json b/x-pack/plugins/alerts/kibana.json index 3509f79dbbe4d..eef61ff4b3d53 100644 --- a/x-pack/plugins/alerts/kibana.json +++ b/x-pack/plugins/alerts/kibana.json @@ -6,5 +6,6 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "alerts"], "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"], - "optionalPlugins": ["usageCollection", "spaces", "security"] + "optionalPlugins": ["usageCollection", "spaces", "security"], + "extraPublicDirs": ["common", "common/parse_duration"] } diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 1b8e7c4dc5431..56a9e226b6528 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -25,5 +25,8 @@ "configPath": [ "xpack", "apm" + ], + "extraPublicDirs": [ + "public/style/variables" ] } diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.ts b/x-pack/plugins/canvas/common/lib/autocomplete.ts index 0ab549bd14e83..c97879de2137e 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.ts @@ -14,7 +14,7 @@ import { ExpressionFunction, ExpressionFunctionParameter, getByAlias, -} from '../../../../../src/plugins/expressions'; +} from '../../../../../src/plugins/expressions/common'; const MARKER = 'CANVAS_SUGGESTION_MARKER'; diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index 443bb63a27799..1be55d2b7a635 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -8,6 +8,7 @@ "requiredPlugins": [ "data" ], + "optionalPlugins": ["kibanaReact", "kibanaUtils"], "server": true, "ui": true } diff --git a/x-pack/plugins/features/kibana.json b/x-pack/plugins/features/kibana.json index 1cab1821b1bf5..92fdd08e93478 100644 --- a/x-pack/plugins/features/kibana.json +++ b/x-pack/plugins/features/kibana.json @@ -6,5 +6,6 @@ "optionalPlugins": ["visTypeTimelion"], "configPath": ["xpack", "features"], "server": true, - "ui": true + "ui": true, + "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index 382ea0444093d..35447139607a6 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,5 +5,6 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud"] + "optionalPlugins": ["security", "features", "cloud"], + "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index a8b22b3e22750..346a5a24c269f 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -13,5 +13,6 @@ "dashboard" ], "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"], - "configPath": ["xpack", "lens"] + "configPath": ["xpack", "lens"], + "extraPublicDirs": ["common/constants"] } diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index be28c8e978d8a..6da923c5cff5a 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -5,5 +5,6 @@ "ui": true, "requiredPlugins": ["home", "licensing", "management"], "optionalPlugins": ["telemetry"], - "configPath": ["xpack", "license_management"] + "configPath": ["xpack", "license_management"], + "extraPublicDirs": ["common/constants"] } diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 67520321de761..f8a30b8d0337e 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -18,5 +18,6 @@ "usageCollection" ], "ui": true, - "server": true + "server": true, + "extraPublicDirs": ["common/constants"] } diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 7e6d0425f2ae0..9483cb67392c4 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -13,5 +13,6 @@ "savedObjectsManagement" ], "server": true, - "ui": true + "ui": true, + "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index d8f5055368831..158cfa100d546 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -5,5 +5,6 @@ "ui": true, "optionalPlugins": ["alerts", "alertingBuiltins"], "requiredPlugins": ["management", "charts", "data"], - "configPath": ["xpack", "trigger_actions_ui"] + "configPath": ["xpack", "trigger_actions_ui"], + "extraPublicDirs": ["public/common", "public/common/constants"] } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 3e37bac0a17af..58344026079e7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -12,7 +12,10 @@ import { UiActionsActionDefinition as ActionDefinition, } from '../../../../../src/plugins/ui_actions/public'; import { defaultState, transitions, selectors, State } from './dynamic_action_manager_state'; -import { StateContainer, createStateContainer } from '../../../../../src/plugins/kibana_utils'; +import { + StateContainer, + createStateContainer, +} from '../../../../../src/plugins/kibana_utils/common'; import { StartContract } from '../plugin'; import { SerializedAction, SerializedEvent } from './types'; From 88eed9819c9888986f148d742377a8a92ecefd8e Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 16 Jun 2020 07:23:10 +0100 Subject: [PATCH 03/68] [CI] GitHub check details link to test reports and errorlevel (#69157) --- .ci/end2end.groovy | 12 ++++++++---- x-pack/plugins/apm/e2e/run-e2e.sh | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 0cd64dcfd41fd..ed642f22cfeb4 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -76,7 +76,7 @@ pipeline { } } steps{ - notifyStatus('Running smoke tests', 'PENDING') + notifyTestStatus('Running smoke tests', 'PENDING') dir("${BASE_DIR}"){ sh "${E2E_DIR}/ci/run-e2e.sh" } @@ -95,10 +95,10 @@ pipeline { } } unsuccessful { - notifyStatus('Test failures', 'FAILURE') + notifyTestStatus('Test failures', 'FAILURE') } success { - notifyStatus('Tests passed', 'SUCCESS') + notifyTestStatus('Tests passed', 'SUCCESS') } } } @@ -113,5 +113,9 @@ pipeline { } def notifyStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanDisplayURL()) + withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) +} + +def notifyTestStatus(String description, String status) { + withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) } diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 4bebab8e0c6a8..aa7c0e21425ad 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -164,6 +164,7 @@ echo "✅ Setup completed successfully. Running tests..." # run cypress tests ################################################## yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true +e2e_status=$? # # Run interactively @@ -171,3 +172,9 @@ yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true echo "${bold}If you want to run the test interactively, run:${normal}" echo "" # newline echo "cd ${E2E_DIR} && yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true" + +# Report the e2e status at the very end +if [ $e2e_status -ne 0 ]; then + echo "⚠️ Running tests failed." + exit 1 +fi From 833ce082591f99baf740292edb298598130100a7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 16 Jun 2020 09:33:10 +0100 Subject: [PATCH 04/68] [Alerting] Adds API for executing actions immediately (#68897) This PR adds an API to the AlertsClient which allows users to execute actions immediately, rather than via TaskManager. This also moves the TaskManager based execution to a new api on ActionsClient along side the immediate execution. --- x-pack/plugins/actions/README.md | 52 +++++- .../actions/server/actions_client.mock.ts | 2 + .../actions/server/actions_client.test.ts | 58 +++++++ .../plugins/actions/server/actions_client.ts | 39 ++++- .../server/create_execute_function.test.ts | 152 ++---------------- .../actions/server/create_execute_function.ts | 49 ++---- x-pack/plugins/actions/server/mocks.ts | 1 - x-pack/plugins/actions/server/plugin.ts | 39 +++-- .../actions/server/routes/execute.test.ts | 89 +++++----- .../plugins/actions/server/routes/execute.ts | 16 +- .../create_execution_handler.test.ts | 35 ++-- .../task_runner/create_execution_handler.ts | 7 +- .../server/task_runner/task_runner.test.ts | 132 ++++++++++++++- .../alerts/server/task_runner/task_runner.ts | 11 +- 14 files changed, 401 insertions(+), 281 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 96d5f04ac088f..605676cee363d 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -234,9 +234,24 @@ Payload: ## Firing actions -The plugin exposes an execute function that you can use to run actions. +Running actions is possible by using the ActionsClient which is provided by the `getActionsClientWithRequest` function part of the plugin's Start Contract. +By providing the user's Request you'll receive an instance of the ActionsClient which is tailered to the current user and is scoped to the resources the user is authorized to access. -**server.plugins.actions.execute(options)** +## Accessing a scoped ActionsClient + +``` +const actionsClient = server.plugins.actions.getActionsClientWithRequest(request); +``` + +Once you have a scoped ActionsClient you can execute an action by caling either the `enqueueExecution` which will schedule the action to run later or the `execute` apis which will run it immediately and return the result respectively. + +### actionsClient.enqueueExecution(options) + +This api schedules a task which will run the action using the current user scope at the soonest opportunity. + +Running the action by scheduling a task means that we will no longer have a user request by which to ascertain the action's privileges and so you might need to provide these yourself: +- The **SpaceId** in which the user's action is expected to run +- When security is enabled you'll also need to provide an **apiKey** which allows us to mimic the user and their privileges. The following table describes the properties of the `options` object. @@ -251,8 +266,9 @@ The following table describes the properties of the `options` object. This example makes action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` send an email. The action plugin will load the saved object and find what action type to call with `params`. -``` -server.plugins.actions.execute({ +```typescript +const actionsClient = await server.plugins.actions.getActionsClientWithRequest(request); +await actionsClient.enqueueExecution({ id: '3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5', spaceId: 'default', // The spaceId of the action params: { @@ -264,6 +280,34 @@ server.plugins.actions.execute({ }); ``` +### actionsClient.execute(options) + +This api runs the action and asynchronously returns the result of running the action. + +The following table describes the properties of the `options` object. + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------ | ------ | +| id | The id of the action you want to execute. | string | +| params | The `params` value to give the action type executor. | object | + +## Example + +As with the previous example, we'll use the action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` to send an email. + +```typescript +const actionsClient = await server.plugins.actions.getActionsClientWithRequest(request); +const result = await actionsClient.execute({ + id: '3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5', + params: { + from: 'example@elastic.co', + to: ['destination@elastic.co'], + subject: 'My email subject', + body: 'My email body', + }, +}); +``` + # Built-in Action Types Kibana ships with a set of built-in action types: diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index a2b64e49f76e3..efd044c7e2493 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -17,6 +17,8 @@ const createActionsClientMock = () => { update: jest.fn(), getAll: jest.fn(), getBulk: jest.fn(), + execute: jest.fn(), + enqueueExecution: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index bf55a1c18d169..2865bbbe1d944 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -19,10 +19,16 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; +import { actionExecutorMock } from './lib/action_executor.mock'; +import uuid from 'uuid'; +import { KibanaRequest } from 'kibana/server'; const defaultKibanaIndex = '.kibana'; const savedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); +const actionExecutor = actionExecutorMock.create(); +const executionEnqueuer = jest.fn(); +const request = {} as KibanaRequest; const mockTaskManager = taskManagerMock.setup(); @@ -53,6 +59,9 @@ beforeEach(() => { scopedClusterClient, defaultKibanaIndex, preconfiguredActions: [], + actionExecutor, + executionEnqueuer, + request, }); }); @@ -232,6 +241,9 @@ describe('create()', () => { scopedClusterClient, defaultKibanaIndex, preconfiguredActions: [], + actionExecutor, + executionEnqueuer, + request, }); const savedObjectCreateResult = { @@ -328,6 +340,9 @@ describe('get()', () => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, preconfiguredActions: [ { id: 'testPreconfigured', @@ -388,6 +403,9 @@ describe('getAll()', () => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, preconfiguredActions: [ { id: 'testPreconfigured', @@ -453,6 +471,9 @@ describe('getBulk()', () => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, preconfiguredActions: [ { id: 'testPreconfigured', @@ -718,3 +739,40 @@ describe('update()', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); }); + +describe('execute()', () => { + test('calls the actionExecutor with the appropriate parameters', async () => { + const actionId = uuid.v4(); + actionExecutor.execute.mockResolvedValue({ status: 'ok', actionId }); + await expect( + actionsClient.execute({ + actionId, + params: { + name: 'my name', + }, + }) + ).resolves.toMatchObject({ status: 'ok', actionId }); + + expect(actionExecutor.execute).toHaveBeenCalledWith({ + actionId, + request, + params: { + name: 'my name', + }, + }); + }); +}); + +describe('enqueueExecution()', () => { + test('calls the executionEnqueuer with the appropriate parameters', async () => { + const opts = { + id: uuid.v4(), + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('123:abc').toString('base64'), + }; + await expect(actionsClient.enqueueExecution(opts)).resolves.toMatchInlineSnapshot(`undefined`); + + expect(executionEnqueuer).toHaveBeenCalledWith(savedObjectsClient, opts); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 48703f01f5509..a2feac83cba9f 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -9,13 +9,25 @@ import { SavedObjectsClientContract, SavedObjectAttributes, SavedObject, + KibanaRequest, } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { ActionTypeRegistry } from './action_type_registry'; -import { validateConfig, validateSecrets } from './lib'; -import { ActionResult, FindActionResult, RawAction, PreConfiguredAction } from './types'; +import { validateConfig, validateSecrets, ActionExecutorContract } from './lib'; +import { + ActionResult, + FindActionResult, + RawAction, + PreConfiguredAction, + ActionTypeExecutorResult, +} from './types'; import { PreconfiguredActionDisabledModificationError } from './lib/errors/preconfigured_action_disabled_modification'; +import { ExecuteOptions } from './lib/action_executor'; +import { + ExecutionEnqueuer, + ExecuteOptions as EnqueueExecutionOptions, +} from './create_execute_function'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -42,6 +54,9 @@ interface ConstructorOptions { actionTypeRegistry: ActionTypeRegistry; savedObjectsClient: SavedObjectsClientContract; preconfiguredActions: PreConfiguredAction[]; + actionExecutor: ActionExecutorContract; + executionEnqueuer: ExecutionEnqueuer; + request: KibanaRequest; } interface UpdateOptions { @@ -55,6 +70,9 @@ export class ActionsClient { private readonly savedObjectsClient: SavedObjectsClientContract; private readonly actionTypeRegistry: ActionTypeRegistry; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly actionExecutor: ActionExecutorContract; + private readonly request: KibanaRequest; + private readonly executionEnqueuer: ExecutionEnqueuer; constructor({ actionTypeRegistry, @@ -62,12 +80,18 @@ export class ActionsClient { scopedClusterClient, savedObjectsClient, preconfiguredActions, + actionExecutor, + executionEnqueuer, + request, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.savedObjectsClient = savedObjectsClient; this.scopedClusterClient = scopedClusterClient; this.defaultKibanaIndex = defaultKibanaIndex; this.preconfiguredActions = preconfiguredActions; + this.actionExecutor = actionExecutor; + this.executionEnqueuer = executionEnqueuer; + this.request = request; } /** @@ -251,6 +275,17 @@ export class ActionsClient { } return await this.savedObjectsClient.delete('action', id); } + + public async execute({ + actionId, + params, + }: Omit): Promise { + return this.actionExecutor.execute({ actionId, params, request: this.request }); + } + + public async enqueueExecution(options: EnqueueExecutionOptions): Promise { + return this.executionEnqueuer(this.savedObjectsClient, options); + } } function actionFromSavedObject(savedObject: SavedObject): ActionResult { diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 1b7752588e3d3..04d4d92945cdb 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -5,23 +5,20 @@ */ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; -import { createExecuteFunction } from './create_execute_function'; +import { createExecutionEnqueuerFunction } from './create_execute_function'; import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { actionTypeRegistryMock } from './action_type_registry.mock'; const mockTaskManager = taskManagerMock.start(); const savedObjectsClient = savedObjectsClientMock.create(); -const getBasePath = jest.fn(); beforeEach(() => jest.resetAllMocks()); describe('execute()', () => { test('schedules the action with all given parameters', async () => { - const executeFn = createExecuteFunction({ - getBasePath, + const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), - getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, preconfiguredActions: [], }); @@ -39,7 +36,7 @@ describe('execute()', () => { attributes: {}, references: [], }); - await executeFn({ + await executeFn(savedObjectsClient, { id: '123', params: { baz: false }, spaceId: 'default', @@ -70,11 +67,9 @@ describe('execute()', () => { }); test('schedules the action with all given parameters with a preconfigured action', async () => { - const executeFn = createExecuteFunction({ - getBasePath, + const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), - getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, preconfiguredActions: [ { @@ -101,7 +96,7 @@ describe('execute()', () => { attributes: {}, references: [], }); - await executeFn({ + await executeFn(savedObjectsClient, { id: '123', params: { baz: false }, spaceId: 'default', @@ -131,115 +126,15 @@ describe('execute()', () => { }); }); - test('uses API key when provided', async () => { - const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); - const executeFn = createExecuteFunction({ - getBasePath, - taskManager: mockTaskManager, - getScopedSavedObjectsClient, - isESOUsingEphemeralEncryptionKey: false, - actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - - await executeFn({ - id: '123', - params: { baz: false }, - spaceId: 'default', - apiKey: Buffer.from('123:abc').toString('base64'), - }); - expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: { - // base64 encoded "123:abc" - authorization: 'ApiKey MTIzOmFiYw==', - }, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }); - }); - - test(`doesn't use API keys when not provided`, async () => { - const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); - const executeFn = createExecuteFunction({ - getBasePath, - taskManager: mockTaskManager, - getScopedSavedObjectsClient, - isESOUsingEphemeralEncryptionKey: false, - actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - - await executeFn({ - id: '123', - params: { baz: false }, - spaceId: 'default', - apiKey: null, - }); - expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: {}, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }); - }); - test('throws when passing isESOUsingEphemeralEncryptionKey with true as a value', async () => { - const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); - const executeFn = createExecuteFunction({ - getBasePath, + const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: true, actionTypeRegistry: actionTypeRegistryMock.create(), preconfiguredActions: [], }); await expect( - executeFn({ + executeFn(savedObjectsClient, { id: '123', params: { baz: false }, spaceId: 'default', @@ -252,11 +147,8 @@ describe('execute()', () => { test('should ensure action type is enabled', async () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); - const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); - const executeFn = createExecuteFunction({ - getBasePath, + const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: mockedActionTypeRegistry, preconfiguredActions: [], @@ -274,7 +166,7 @@ describe('execute()', () => { }); await expect( - executeFn({ + executeFn(savedObjectsClient, { id: '123', params: { baz: false }, spaceId: 'default', @@ -285,11 +177,8 @@ describe('execute()', () => { test('should skip ensure action type if action type is preconfigured and license is valid', async () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); - const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); - const executeFn = createExecuteFunction({ - getBasePath, + const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: mockedActionTypeRegistry, preconfiguredActions: [ @@ -303,9 +192,6 @@ describe('execute()', () => { }, ], }); - mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { - throw new Error('Fail'); - }); mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -322,25 +208,13 @@ describe('execute()', () => { references: [], }); - await executeFn({ + await executeFn(savedObjectsClient, { id: '123', params: { baz: false }, spaceId: 'default', apiKey: null, }); - expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: {}, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }); + + expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index a16b2f4295072..2bad33d56f228 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, KibanaRequest } from '../../../../src/core/server'; +import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { - GetBasePathFunction, - RawAction, - ActionTypeRegistryContract, - PreConfiguredAction, -} from './types'; +import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; - getScopedSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; - getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; preconfiguredActions: PreConfiguredAction[]; @@ -29,45 +22,27 @@ export interface ExecuteOptions { apiKey: string | null; } -export function createExecuteFunction({ - getBasePath, +export type ExecutionEnqueuer = ( + savedObjectsClient: SavedObjectsClientContract, + options: ExecuteOptions +) => Promise; + +export function createExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, - getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey, preconfiguredActions, }: CreateExecuteFunctionOptions) { - return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) { + return async function execute( + savedObjectsClient: SavedObjectsClientContract, + { id, params, spaceId, apiKey }: ExecuteOptions + ) { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` ); } - const requestHeaders: Record = {}; - - if (apiKey) { - requestHeaders.authorization = `ApiKey ${apiKey}`; - } - - // Since we're using API keys and accessing elasticsearch can only be done - // via a request, we're faking one with the proper authorization headers. - const fakeRequest: unknown = { - headers: requestHeaders, - getBasePath: () => getBasePath(spaceId), - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }; - - const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest); const actionTypeId = await getActionTypeId(id); if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 09bc137da736f..1763d275c6fb0 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -23,7 +23,6 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { - execute: jest.fn(), isActionTypeEnabled: jest.fn(), isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 8c1b21e600ae1..ad3fa97ee0c36 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -35,8 +35,7 @@ import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; -import { ExecuteOptions } from './create_execute_function'; -import { createExecuteFunction } from './create_execute_function'; +import { createExecutionEnqueuerFunction } from './create_execute_function'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { registerActionsUsageCollector } from './usage'; @@ -68,7 +67,6 @@ export interface PluginSetupContract { export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; isActionExecutable(actionId: string, actionTypeId: string): boolean; - execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; preconfiguredActions: PreConfiguredAction[]; } @@ -203,7 +201,7 @@ export class ActionsPlugin implements Plugin, Plugi getAllActionRoute(router, this.licenseState); updateActionRoute(router, this.licenseState); listActionTypesRoute(router, this.licenseState); - executeActionRoute(router, this.licenseState, actionExecutor); + executeActionRoute(router, this.licenseState); return { registerType: (actionType: ActionType) => { @@ -269,14 +267,6 @@ export class ActionsPlugin implements Plugin, Plugi scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); return { - execute: createExecuteFunction({ - taskManager: plugins.taskManager, - actionTypeRegistry: actionTypeRegistry!, - getScopedSavedObjectsClient, - getBasePath: this.getBasePath, - isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, - preconfiguredActions, - }), isActionTypeEnabled: (id) => { return this.actionTypeRegistry!.isActionTypeEnabled(id); }, @@ -296,6 +286,14 @@ export class ActionsPlugin implements Plugin, Plugi defaultKibanaIndex: await kibanaIndex, scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, + request, + actionExecutor: actionExecutor!, + executionEnqueuer: createExecutionEnqueuerFunction({ + taskManager: plugins.taskManager, + actionTypeRegistry: actionTypeRegistry!, + isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, + preconfiguredActions, + }), }); }, preconfiguredActions, @@ -319,10 +317,15 @@ export class ActionsPlugin implements Plugin, Plugi core: CoreSetup, defaultKibanaIndex: string ): IContextProvider, 'actions'> => { - const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; + const { + actionTypeRegistry, + isESOUsingEphemeralEncryptionKey, + preconfiguredActions, + actionExecutor, + } = this; return async function actionsRouteHandlerContext(context, request) { - const [{ savedObjects }] = await core.getStartServices(); + const [{ savedObjects }, { taskManager }] = await core.getStartServices(); return { getActionsClient: () => { if (isESOUsingEphemeralEncryptionKey === true) { @@ -336,6 +339,14 @@ export class ActionsPlugin implements Plugin, Plugi defaultKibanaIndex, scopedClusterClient: context.core.elasticsearch.legacy.client, preconfiguredActions, + request, + actionExecutor: actionExecutor!, + executionEnqueuer: createExecutionEnqueuerFunction({ + taskManager, + actionTypeRegistry: actionTypeRegistry!, + isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, + preconfiguredActions, + }), }); }, listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!), diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 4467ddf372b21..6e8ebbf6f91cd 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -8,7 +8,9 @@ import { executeActionRoute } from './execute'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; +import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; +import { actionsClientMock } from '../actions_client.mock'; +import { ActionTypeExecutorResult } from '../types'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -23,8 +25,11 @@ describe('executeActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValueOnce({ status: 'ok', actionId: '1' }); + const [context, req, res] = mockHandlerArguments( - {}, + { actionsClient }, { body: { params: { @@ -42,14 +47,8 @@ describe('executeActionRoute', () => { actionId: '1', status: 'ok', }; - const actionExecutor = { - initialize: jest.fn(), - execute: jest.fn(async ({ params, request, actionId }) => { - return executeResult; - }), - } as jest.Mocked; - executeActionRoute(router, licenseState, actionExecutor); + executeActionRoute(router, licenseState); const [config, handler] = router.post.mock.calls[0]; @@ -64,12 +63,11 @@ describe('executeActionRoute', () => { expect(await handler(context, req, res)).toEqual({ body: executeResult }); - expect(actionExecutor.execute).toHaveBeenCalledWith({ + expect(actionsClient.execute).toHaveBeenCalledWith({ actionId: '1', params: { someData: 'data', }, - request: req, }); expect(res.ok).toHaveBeenCalled(); @@ -79,8 +77,11 @@ describe('executeActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValueOnce((null as unknown) as ActionTypeExecutorResult); + const [context, req, res] = mockHandlerArguments( - {}, + { actionsClient }, { body: { params: {}, @@ -92,21 +93,15 @@ describe('executeActionRoute', () => { ['noContent'] ); - const actionExecutor = { - initialize: jest.fn(), - execute: jest.fn(), - } as jest.Mocked; - - executeActionRoute(router, licenseState, actionExecutor); + executeActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; expect(await handler(context, req, res)).toEqual(undefined); - expect(actionExecutor.execute).toHaveBeenCalledWith({ + expect(actionsClient.execute).toHaveBeenCalledWith({ actionId: '1', params: {}, - request: req, }); expect(res.ok).not.toHaveBeenCalled(); @@ -117,8 +112,14 @@ describe('executeActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + const [context, req, res] = mockHandlerArguments( - {}, + { actionsClient }, { body: {}, params: {}, @@ -126,17 +127,7 @@ describe('executeActionRoute', () => { ['ok'] ); - const actionExecutor = { - initialize: jest.fn(), - execute: jest.fn(async ({ params, request, actionId }) => { - return { - actionId: '1', - status: 'ok', - }; - }), - } as jest.Mocked; - - executeActionRoute(router, licenseState, actionExecutor); + executeActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -149,12 +140,18 @@ describe('executeActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); }); const [context, req, res] = mockHandlerArguments( - {}, + { actionsClient }, { body: {}, params: {}, @@ -162,17 +159,7 @@ describe('executeActionRoute', () => { ['ok'] ); - const actionExecutor = { - initialize: jest.fn(), - execute: jest.fn(async ({ params, request, actionId }) => { - return { - actionId: '1', - status: 'ok', - }; - }), - } as jest.Mocked; - - executeActionRoute(router, licenseState, actionExecutor); + executeActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -185,8 +172,11 @@ describe('executeActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); + const [context, req, res] = mockHandlerArguments( - {}, + { actionsClient }, { body: {}, params: {}, @@ -194,14 +184,7 @@ describe('executeActionRoute', () => { ['ok', 'forbidden'] ); - const actionExecutor = { - initialize: jest.fn(), - execute: jest.fn().mockImplementation(() => { - throw new ActionTypeDisabledError('Fail', 'license_invalid'); - }), - } as jest.Mocked; - - executeActionRoute(router, licenseState, actionExecutor); + executeActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 820bc191993d1..28e6a54f5e92d 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -13,7 +13,6 @@ import { } from 'kibana/server'; import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; -import { ActionExecutorContract } from '../lib'; import { ActionTypeExecutorResult } from '../types'; import { BASE_ACTION_API_PATH } from '../../common'; @@ -25,11 +24,7 @@ const bodySchema = schema.object({ params: schema.recordOf(schema.string(), schema.any()), }); -export const executeActionRoute = ( - router: IRouter, - licenseState: ILicenseState, - actionExecutor: ActionExecutorContract -) => { +export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ACTION_API_PATH}/action/{id}/_execute`, @@ -47,12 +42,17 @@ export const executeActionRoute = ( res: KibanaResponseFactory ): Promise { verifyApiAccess(licenseState); + + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + + const actionsClient = context.actions.getActionsClient(); const { params } = req.body; const { id } = req.params; try { - const body: ActionTypeExecutorResult = await actionExecutor.execute({ + const body: ActionTypeExecutorResult = await actionsClient.execute({ params, - request: req, actionId: id, }); return body diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index ceae9f04fe5ae..dd5a9f531bd58 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -7,8 +7,9 @@ import { AlertType } from '../types'; import { createExecutionHandler } from './create_execution_handler'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; -import { actionsMock } from '../../../actions/server/mocks'; +import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; +import { KibanaRequest } from 'kibana/server'; const alertType: AlertType = { id: 'test', @@ -22,6 +23,7 @@ const alertType: AlertType = { producer: 'alerting', }; +const actionsClient = actionsClientMock.create(); const createExecutionHandlerParams = { actionsPlugin: actionsMock.createStart(), spaceId: 'default', @@ -47,15 +49,19 @@ const createExecutionHandlerParams = { }, }, ], + request: {} as KibanaRequest, }; beforeEach(() => { jest.resetAllMocks(); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( + actionsClient + ); }); -test('calls actionsPlugin.execute per selected action', async () => { +test('enqueues execution per selected action', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ actionGroup: 'default', @@ -63,8 +69,11 @@ test('calls actionsPlugin.execute per selected action', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` + expect( + createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest + ).toHaveBeenCalledWith(createExecutionHandlerParams.request); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -139,8 +148,8 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledWith({ + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution).toHaveBeenCalledWith({ id: '2', params: { foo: true, @@ -180,7 +189,7 @@ test('trow error error message when action type is disabled', async () => { alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(0); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0); createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true); const executionHandlerForPreconfiguredAction = createExecutionHandler({ @@ -193,7 +202,7 @@ test('trow error error message when action type is disabled', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); }); test('limits actionsPlugin.execute per action group', async () => { @@ -204,7 +213,7 @@ test('limits actionsPlugin.execute per action group', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).not.toHaveBeenCalled(); + expect(actionsClient.enqueueExecution).not.toHaveBeenCalled(); }); test('context attribute gets parameterized', async () => { @@ -215,8 +224,8 @@ test('context attribute gets parameterized', async () => { state: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -241,8 +250,8 @@ test('state attribute gets parameterized', async () => { state: { value: 'state-val' }, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 3c58c6d9ba288..8d859a570ba91 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -6,7 +6,7 @@ import { pluck } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; -import { Logger } from '../../../../../src/core/server'; +import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; @@ -23,6 +23,7 @@ interface CreateExecutionHandlerOptions { alertType: AlertType; logger: Logger; eventLogger: IEventLogger; + request: KibanaRequest; } interface ExecutionHandlerOptions { @@ -43,6 +44,7 @@ export function createExecutionHandler({ apiKey, alertType, eventLogger, + request, }: CreateExecutionHandlerOptions) { const alertTypeActionGroups = new Set(pluck(alertType.actionGroups, 'id')); return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => { @@ -80,7 +82,8 @@ export function createExecutionHandler({ // TODO would be nice to add the action name here, but it's not available const actionLabel = `${action.actionTypeId}:${action.id}`; - await actionsPlugin.execute({ + const actionsClient = await actionsPlugin.getActionsClientWithRequest(request); + await actionsClient.enqueueExecution({ id: action.id, params: action.params, spaceId, diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 983dff86d5602..690971bc87006 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -13,7 +13,7 @@ import { TaskRunner } from './task_runner'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; -import { actionsMock } from '../../../actions/server/mocks'; +import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; import { alertsMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; @@ -57,6 +57,7 @@ describe('Task Runner', () => { const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const services = alertsMock.createAlertServices(); const savedObjectsClient = services.savedObjectsClient; + const actionsClient = actionsClientMock.create(); const taskRunnerFactoryInitializerParams: jest.Mocked & { actionsPlugin: jest.Mocked; @@ -108,6 +109,9 @@ describe('Task Runner', () => { beforeEach(() => { jest.resetAllMocks(); taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services); + taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( + actionsClient + ); }); test('successfully executes the task', async () => { @@ -206,9 +210,129 @@ describe('Task Runner', () => { references: [], }); await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute.mock.calls[0]) - .toMatchInlineSnapshot(` + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "foo": true, + }, + "spaceId": undefined, + }, + ] + `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent).toHaveBeenCalledWith({ + event: { + action: 'execute', + outcome: 'success', + }, + kibana: { + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "alert executed: test:1: 'alert-name'", + }); + expect(eventLogger.logEvent).toHaveBeenCalledWith({ + event: { + action: 'new-instance', + }, + kibana: { + alerting: { + instance_id: '1', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "test:1: 'alert-name' created new instance: '1'", + }); + expect(eventLogger.logEvent).toHaveBeenCalledWith({ + event: { + action: 'execute-action', + }, + kibana: { + alerting: { + instance_id: '1', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + { + id: '1', + namespace: undefined, + type: 'action', + }, + ], + }, + message: + "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1", + }); + }); + + test('includes the apiKey in the request used to initialize the actionsClient', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + ({ services: executorServices }: AlertExecutorOptions) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + expect( + taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest + ).toHaveBeenCalledWith({ + getBasePath: expect.anything(), + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index be399893088e3..3512ab16a3712 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -71,14 +71,14 @@ export class TaskRunner { return apiKey; } - async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) { + private getFakeKibanaRequest(spaceId: string, apiKey: string | null) { const requestHeaders: Record = {}; if (apiKey) { requestHeaders.authorization = `ApiKey ${apiKey}`; } - const fakeRequest = { + return ({ headers: requestHeaders, getBasePath: () => this.context.getBasePath(spaceId), path: '/', @@ -91,9 +91,11 @@ export class TaskRunner { url: '/', }, }, - }; + } as unknown) as KibanaRequest; + } - return this.context.getServices((fakeRequest as unknown) as KibanaRequest); + async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) { + return this.context.getServices(this.getFakeKibanaRequest(spaceId, apiKey)); } private getExecutionHandler( @@ -128,6 +130,7 @@ export class TaskRunner { spaceId, alertType: this.alertType, eventLogger: this.context.eventLogger, + request: this.getFakeKibanaRequest(spaceId, apiKey), }); } From 721125772f32d4780409000516e5eddbf856069d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 16 Jun 2020 10:42:37 +0200 Subject: [PATCH 05/68] stabilize flaky embeddables/adding_children take 2 (#68873) * wait for loading indicator to be deleted * improve with retry * Update test/examples/embeddables/adding_children.ts Co-authored-by: Spencer Co-authored-by: Spencer Co-authored-by: Elastic Machine --- test/examples/embeddables/adding_children.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index 4e3d9d30b01aa..9c088f0a5d313 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -24,6 +24,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services export default function ({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); const flyout = getService('flyout'); + const retry = getService('retry'); describe('creating and adding children', () => { before(async () => { @@ -33,8 +34,15 @@ export default function ({ getService }: PluginFunctionalProviderContext) { it('Can create a new child', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); - await testSubjects.click('createNew'); + + // this seem like an overkill, but clicking this button which opens context menu was flaky + await testSubjects.waitForEnabled('createNew'); + await retry.waitFor('createNew popover opened', async () => { + await testSubjects.click('createNew'); + return await testSubjects.exists('createNew-TODO_EMBEDDABLE'); + }); await testSubjects.click('createNew-TODO_EMBEDDABLE'); + await testSubjects.setValue('taskInputField', 'new task'); await testSubjects.click('createTodoEmbeddable'); const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); @@ -44,6 +52,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { it('Can add a child backed off a saved object', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); + await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); await testSubjects.click('savedObjectTitleGarbage'); await testSubjects.moveMouseTo('euiFlyoutCloseButton'); await flyout.ensureClosed('dashboardAddPanel'); From c98845c7c78965edc882d60292e61f89f8ff95c2 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 16 Jun 2020 12:35:34 +0300 Subject: [PATCH 06/68] Update vega version (#68639) * update vega version * a11y skip test * a11y skip test attempt 2 * adding back all a11y tests * fix failed karma test * remove extra 0BSD license * coalesce yarn.lock versions a little * update kbn/pm dist * fix CI * fix Vega View for ML Co-authored-by: Michail Yasonik Co-authored-by: spalger Co-authored-by: Elastic Machine --- docs/visualize/vega.asciidoc | 2 +- package.json | 6 +- .../src/worker/webpack.config.ts | 2 +- packages/kbn-pm/dist/index.js | 3145 +++++++++-------- .../__tests__/vis_type_vega/vega_graph.hjson | 2 +- .../vis_type_vega/vega_map_test.hjson | 2 +- .../vis_type_vega/vega_tooltip_test.hjson | 2 +- .../vis_type_vega/vega_visualization.js | 7 +- .../vis_type_vega/vegalite_graph.hjson | 2 +- .../ui/ui_exports/ui_export_defaults.js | 2 +- .../data_sets/flights/saved_objects.ts | 2 +- .../data_sets/logs/saved_objects.ts | 4 +- .../public/data_model/vega_parser.js | 7 +- .../public/data_model/vega_parser.test.js | 18 +- .../vis_type_vega/public/default.spec.hjson | 2 +- src/plugins/vis_type_vega/public/lib/vega.js | 23 + .../public/vega_view/vega_base_view.js | 3 +- .../public/vega_view/vega_map_view.js | 2 +- .../public/vega_view/vega_view.js | 2 +- test/accessibility/services/a11y/a11y.ts | 7 +- x-pack/package.json | 1 + .../__snapshots__/CustomPlot.test.js.snap | 66 +- .../__snapshots__/Histogram.test.js.snap | 20 +- ...ditbeat_docker_process_occurrence_ecs.json | 2 +- ...uditbeat_hosts_process_occurrence_ecs.json | 2 +- yarn.lock | 927 +++-- 26 files changed, 2195 insertions(+), 2065 deletions(-) create mode 100644 src/plugins/vis_type_vega/public/lib/vega.js diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index efe9094a14922..5f127df459ed3 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -32,7 +32,7 @@ image::images/vega_lite_default.png[] The default visualization uses Vega-Lite version 2. To use Vega version 4, edit the `schema`. -Go to `$schema`, enter `https://vega.github.io/schema/vega/v4.json`, then click +Go to `$schema`, enter `https://vega.github.io/schema/vega/v5.json`, then click *Update*. [float] diff --git a/package.json b/package.json index e8b07ae4abba2..89f380e5a7a0a 100644 --- a/package.json +++ b/package.json @@ -280,9 +280,9 @@ "uuid": "3.3.2", "val-loader": "^1.1.1", "validate-npm-package-name": "2.2.2", - "vega-lib": "4.3.0", - "vega-lite": "^2.6.0", - "vega-schema-url-parser": "1.0.0", + "vega": "^5.13.0", + "vega-lite": "^4.13.1", + "vega-schema-url-parser": "^1.1.0", "vega-tooltip": "^0.12.0", "vision": "^5.3.3", "webpack": "^4.41.5", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3daf21cdc38cc..e361b186c30e0 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -81,7 +81,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: noParse: [ /[\/\\]node_modules[\/\\]elasticsearch-browser[\/\\]/, /[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/, - /[\/\\]node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, + /[\/\\]node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/, ], rules: [ diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 13bddfd0f110a..1600d66ec7558 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(497); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -105,10 +105,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(162); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(270); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(271); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(271); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(272); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(126); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(489); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(490); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(142); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -1576,10 +1576,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__metadata", function() { return __metadata; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__awaiter", function() { return __awaiter; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__generator", function() { return __generator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__createBinding", function() { return __createBinding; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__exportStar", function() { return __exportStar; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__values", function() { return __values; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__read", function() { return __read; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spread", function() { return __spread; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spreadArrays", function() { return __spreadArrays; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__await", function() { return __await; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncGenerator", function() { return __asyncGenerator; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncDelegator", function() { return __asyncDelegator; }); @@ -1587,19 +1589,21 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__makeTemplateObject", function() { return __makeTemplateObject; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importStar", function() { return __importStar; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importDefault", function() { return __importDefault; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldGet", function() { return __classPrivateFieldGet; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldSet", function() { return __classPrivateFieldSet; }); /*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 +Copyright (c) Microsoft Corporation. -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ @@ -1632,8 +1636,10 @@ function __rest(s, e) { for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0) - t[p[i]] = s[p[i]]; + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } return t; } @@ -1653,10 +1659,11 @@ function __metadata(metadataKey, metadataValue) { } function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } @@ -1689,19 +1696,25 @@ function __generator(thisArg, body) { } } +function __createBinding(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +} + function __exportStar(m, exports) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) exports[p] = m[p]; } function __values(o) { - var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); - return { + if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { @@ -1727,6 +1740,14 @@ function __spread() { return ar; } +function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; +}; + function __await(v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } @@ -1773,6 +1794,21 @@ function __importStar(mod) { function __importDefault(mod) { return (mod && mod.__esModule) ? mod : { default: mod }; } + +function __classPrivateFieldGet(receiver, privateMap) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to get private field on non-instance"); + } + return privateMap.get(receiver); +} + +function __classPrivateFieldSet(receiver, privateMap, value) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to set private field on non-instance"); + } + privateMap.set(receiver, value); + return value; +} /***/ }), @@ -8693,9 +8729,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(127); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(279); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(388); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(389); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(280); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(389); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(390); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8736,8 +8772,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(142); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(143); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(272); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(278); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(273); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(279); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -10745,7 +10781,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); /* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(162); -/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(270); +/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(271); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -14356,7 +14392,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(161); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(142); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(163); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(225); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(226); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -14603,7 +14639,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isLinkDependency", function() { return isLinkDependency; }); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(164); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(read_pkg__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); /* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(write_pkg__WEBPACK_IMPORTED_MODULE_1__); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -16200,7 +16236,7 @@ module.exports = normalize var fixer = __webpack_require__(181) normalize.fixer = fixer -var makeWarning = __webpack_require__(203) +var makeWarning = __webpack_require__(204) var fieldsToFix = ['name','version','description','repository','modules','scripts' ,'files','bin','man','bugs','keywords','readme','homepage','license'] @@ -16245,9 +16281,9 @@ var validateLicense = __webpack_require__(183); var hostedGitInfo = __webpack_require__(188) var isBuiltinModule = __webpack_require__(192).isCore var depTypes = ["dependencies","devDependencies","optionalDependencies"] -var extractDescription = __webpack_require__(201) +var extractDescription = __webpack_require__(202) var url = __webpack_require__(189) -var typos = __webpack_require__(202) +var typos = __webpack_require__(203) var fixer = module.exports = { // default warning function @@ -20197,13 +20233,11 @@ GitHost.prototype.toString = function (opts) { /* 192 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(193); -var async = __webpack_require__(195); -async.core = core; -async.isCore = function isCore(x) { return core[x]; }; -async.sync = __webpack_require__(200); +var async = __webpack_require__(193); +async.core = __webpack_require__(199); +async.isCore = __webpack_require__(198); +async.sync = __webpack_require__(201); -exports = async; module.exports = async; @@ -20211,77 +20245,14 @@ module.exports = async; /* 193 */ /***/ (function(module, exports, __webpack_require__) { -var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; - -function specifierIncluded(specifier) { - var parts = specifier.split(' '); - var op = parts.length > 1 ? parts[0] : '='; - var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); - - for (var i = 0; i < 3; ++i) { - var cur = Number(current[i] || 0); - var ver = Number(versionParts[i] || 0); - if (cur === ver) { - continue; // eslint-disable-line no-restricted-syntax, no-continue - } - if (op === '<') { - return cur < ver; - } else if (op === '>=') { - return cur >= ver; - } else { - return false; - } - } - return op === '>='; -} - -function matchesRange(range) { - var specifiers = range.split(/ ?&& ?/); - if (specifiers.length === 0) { return false; } - for (var i = 0; i < specifiers.length; ++i) { - if (!specifierIncluded(specifiers[i])) { return false; } - } - return true; -} - -function versionIncluded(specifierValue) { - if (typeof specifierValue === 'boolean') { return specifierValue; } - if (specifierValue && typeof specifierValue === 'object') { - for (var i = 0; i < specifierValue.length; ++i) { - if (matchesRange(specifierValue[i])) { return true; } - } - return false; - } - return matchesRange(specifierValue); -} - -var data = __webpack_require__(194); - -var core = {}; -for (var mod in data) { // eslint-disable-line no-restricted-syntax - if (Object.prototype.hasOwnProperty.call(data, mod)) { - core[mod] = versionIncluded(data[mod]); - } -} -module.exports = core; - - -/***/ }), -/* 194 */ -/***/ (function(module) { - -module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":\">= 10 && < 10.1\",\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8\":\">= 1\",\"vm\":true,\"worker_threads\":\">= 11.7\",\"zlib\":true}"); - -/***/ }), -/* 195 */ -/***/ (function(module, exports, __webpack_require__) { - -var core = __webpack_require__(193); var fs = __webpack_require__(132); var path = __webpack_require__(4); -var caller = __webpack_require__(196); -var nodeModulesPaths = __webpack_require__(197); -var normalizeOptions = __webpack_require__(199); +var caller = __webpack_require__(194); +var nodeModulesPaths = __webpack_require__(195); +var normalizeOptions = __webpack_require__(197); +var isCore = __webpack_require__(198); + +var realpathFS = fs.realpath && typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath; var defaultIsFile = function isFile(file, cb) { fs.stat(file, function (err, stat) { @@ -20293,6 +20264,39 @@ var defaultIsFile = function isFile(file, cb) { }); }; +var defaultIsDir = function isDirectory(dir, cb) { + fs.stat(dir, function (err, stat) { + if (!err) { + return cb(null, stat.isDirectory()); + } + if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false); + return cb(err); + }); +}; + +var defaultRealpath = function realpath(x, cb) { + realpathFS(x, function (realpathErr, realPath) { + if (realpathErr && realpathErr.code !== 'ENOENT') cb(realpathErr); + else cb(null, realpathErr ? x : realPath); + }); +}; + +var maybeRealpath = function maybeRealpath(realpath, x, opts, cb) { + if (opts && opts.preserveSymlinks === false) { + realpath(x, cb); + } else { + cb(null, x); + } +}; + +var getPackageCandidates = function getPackageCandidates(x, start, opts) { + var dirs = nodeModulesPaths(start, opts, x); + for (var i = 0; i < dirs.length; i++) { + dirs[i] = path.join(dirs[i], x); + } + return dirs; +}; + module.exports = function resolve(x, options, callback) { var cb = callback; var opts = options; @@ -20310,7 +20314,10 @@ module.exports = function resolve(x, options, callback) { opts = normalizeOptions(x, opts); var isFile = opts.isFile || defaultIsFile; + var isDirectory = opts.isDirectory || defaultIsDir; var readFile = opts.readFile || fs.readFile; + var realpath = opts.realpath || defaultRealpath; + var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; var basedir = opts.basedir || path.dirname(caller()); @@ -20321,28 +20328,37 @@ module.exports = function resolve(x, options, callback) { // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory var absoluteStart = path.resolve(basedir); - if (opts.preserveSymlinks === false) { - fs.realpath(absoluteStart, function (realPathErr, realStart) { - if (realPathErr && realPathErr.code !== 'ENOENT') cb(err); - else init(realPathErr ? absoluteStart : realStart); - }); - } else { - init(absoluteStart); - } + maybeRealpath( + realpath, + absoluteStart, + opts, + function (err, realStart) { + if (err) cb(err); + else init(realStart); + } + ); var res; function init(basedir) { if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) { res = path.resolve(basedir, x); - if (x === '..' || x.slice(-1) === '/') res += '/'; + if (x === '.' || x === '..' || x.slice(-1) === '/') res += '/'; if ((/\/$/).test(x) && res === basedir) { loadAsDirectory(res, opts.package, onfile); } else loadAsFile(res, opts.package, onfile); + } else if (isCore(x)) { + return cb(null, x); } else loadNodeModules(x, basedir, function (err, n, pkg) { if (err) cb(err); - else if (n) cb(null, n, pkg); - else if (core[x]) return cb(null, x); - else { + else if (n) { + return maybeRealpath(realpath, n, opts, function (err, realN) { + if (err) { + cb(err); + } else { + cb(null, realN, pkg); + } + }); + } else { var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'"); moduleError.code = 'MODULE_NOT_FOUND'; cb(moduleError); @@ -20355,8 +20371,15 @@ module.exports = function resolve(x, options, callback) { else if (m) cb(null, m, pkg); else loadAsDirectory(res, function (err, d, pkg) { if (err) cb(err); - else if (d) cb(null, d, pkg); - else { + else if (d) { + maybeRealpath(realpath, d, opts, function (err, realD) { + if (err) { + cb(err); + } else { + cb(null, realD, pkg); + } + }); + } else { var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'"); moduleError.code = 'MODULE_NOT_FOUND'; cb(moduleError); @@ -20413,19 +20436,22 @@ module.exports = function resolve(x, options, callback) { } if ((/[/\\]node_modules[/\\]*$/).test(dir)) return cb(null); - var pkgfile = path.join(dir, 'package.json'); - isFile(pkgfile, function (err, ex) { - // on err, ex is false - if (!ex) return loadpkg(path.dirname(dir), cb); + maybeRealpath(realpath, dir, opts, function (unwrapErr, pkgdir) { + if (unwrapErr) return loadpkg(path.dirname(dir), cb); + var pkgfile = path.join(pkgdir, 'package.json'); + isFile(pkgfile, function (err, ex) { + // on err, ex is false + if (!ex) return loadpkg(path.dirname(dir), cb); - readFile(pkgfile, function (err, body) { - if (err) cb(err); - try { var pkg = JSON.parse(body); } catch (jsonErr) {} + readFile(pkgfile, function (err, body) { + if (err) cb(err); + try { var pkg = JSON.parse(body); } catch (jsonErr) {} - if (pkg && opts.packageFilter) { - pkg = opts.packageFilter(pkg, pkgfile); - } - cb(null, pkg, dir); + if (pkg && opts.packageFilter) { + pkg = opts.packageFilter(pkg, pkgfile); + } + cb(null, pkg, dir); + }); }); }); } @@ -20438,46 +20464,49 @@ module.exports = function resolve(x, options, callback) { fpkg = opts.package; } - var pkgfile = path.join(x, 'package.json'); - isFile(pkgfile, function (err, ex) { - if (err) return cb(err); - if (!ex) return loadAsFile(path.join(x, 'index'), fpkg, cb); - - readFile(pkgfile, function (err, body) { + maybeRealpath(realpath, x, opts, function (unwrapErr, pkgdir) { + if (unwrapErr) return cb(unwrapErr); + var pkgfile = path.join(pkgdir, 'package.json'); + isFile(pkgfile, function (err, ex) { if (err) return cb(err); - try { - var pkg = JSON.parse(body); - } catch (jsonErr) {} + if (!ex) return loadAsFile(path.join(x, 'index'), fpkg, cb); - if (opts.packageFilter) { - pkg = opts.packageFilter(pkg, pkgfile); - } + readFile(pkgfile, function (err, body) { + if (err) return cb(err); + try { + var pkg = JSON.parse(body); + } catch (jsonErr) {} - if (pkg.main) { - if (typeof pkg.main !== 'string') { - var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string'); - mainError.code = 'INVALID_PACKAGE_MAIN'; - return cb(mainError); - } - if (pkg.main === '.' || pkg.main === './') { - pkg.main = 'index'; + if (pkg && opts.packageFilter) { + pkg = opts.packageFilter(pkg, pkgfile); } - loadAsFile(path.resolve(x, pkg.main), pkg, function (err, m, pkg) { - if (err) return cb(err); - if (m) return cb(null, m, pkg); - if (!pkg) return loadAsFile(path.join(x, 'index'), pkg, cb); - var dir = path.resolve(x, pkg.main); - loadAsDirectory(dir, pkg, function (err, n, pkg) { + if (pkg && pkg.main) { + if (typeof pkg.main !== 'string') { + var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string'); + mainError.code = 'INVALID_PACKAGE_MAIN'; + return cb(mainError); + } + if (pkg.main === '.' || pkg.main === './') { + pkg.main = 'index'; + } + loadAsFile(path.resolve(x, pkg.main), pkg, function (err, m, pkg) { if (err) return cb(err); - if (n) return cb(null, n, pkg); - loadAsFile(path.join(x, 'index'), pkg, cb); + if (m) return cb(null, m, pkg); + if (!pkg) return loadAsFile(path.join(x, 'index'), pkg, cb); + + var dir = path.resolve(x, pkg.main); + loadAsDirectory(dir, pkg, function (err, n, pkg) { + if (err) return cb(err); + if (n) return cb(null, n, pkg); + loadAsFile(path.join(x, 'index'), pkg, cb); + }); }); - }); - return; - } + return; + } - loadAsFile(path.join(x, '/index'), pkg, cb); + loadAsFile(path.join(x, '/index'), pkg, cb); + }); }); }); } @@ -20486,13 +20515,18 @@ module.exports = function resolve(x, options, callback) { if (dirs.length === 0) return cb(null, undefined); var dir = dirs[0]; - var file = path.join(dir, x); - loadAsFile(file, opts.package, onfile); + isDirectory(path.dirname(dir), isdir); + + function isdir(err, isdir) { + if (err) return cb(err); + if (!isdir) return processDirs(cb, dirs.slice(1)); + loadAsFile(dir, opts.package, onfile); + } function onfile(err, m, pkg) { if (err) return cb(err); if (m) return cb(null, m, pkg); - loadAsDirectory(path.join(dir, x), opts.package, ondir); + loadAsDirectory(dir, opts.package, ondir); } function ondir(err, n, pkg) { @@ -20502,13 +20536,17 @@ module.exports = function resolve(x, options, callback) { } } function loadNodeModules(x, start, cb) { - processDirs(cb, nodeModulesPaths(start, opts, x)); + var thunk = function () { return getPackageCandidates(x, start, opts); }; + processDirs( + cb, + packageIterator ? packageIterator(x, start, thunk, opts) : thunk() + ); } }; /***/ }), -/* 196 */ +/* 194 */ /***/ (function(module, exports) { module.exports = function () { @@ -20522,11 +20560,11 @@ module.exports = function () { /***/ }), -/* 197 */ +/* 195 */ /***/ (function(module, exports, __webpack_require__) { var path = __webpack_require__(4); -var parse = path.parse || __webpack_require__(198); +var parse = path.parse || __webpack_require__(196); var getNodeModulesDirs = function getNodeModulesDirs(absoluteStart, modules) { var prefix = '/'; @@ -20545,7 +20583,7 @@ var getNodeModulesDirs = function getNodeModulesDirs(absoluteStart, modules) { return paths.reduce(function (dirs, aPath) { return dirs.concat(modules.map(function (moduleDir) { - return path.join(prefix, aPath, moduleDir); + return path.resolve(prefix, aPath, moduleDir); })); }, []); }; @@ -20570,7 +20608,7 @@ module.exports = function nodeModulesPaths(start, opts, request) { /***/ }), -/* 198 */ +/* 196 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -20670,7 +20708,7 @@ module.exports.win32 = win32.parse; /***/ }), -/* 199 */ +/* 197 */ /***/ (function(module, exports) { module.exports = function (x, opts) { @@ -20685,16 +20723,94 @@ module.exports = function (x, opts) { }; +/***/ }), +/* 198 */ +/***/ (function(module, exports, __webpack_require__) { + +var core = __webpack_require__(199); + +module.exports = function isCore(x) { + return Object.prototype.hasOwnProperty.call(core, x); +}; + + +/***/ }), +/* 199 */ +/***/ (function(module, exports, __webpack_require__) { + +var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; + +function specifierIncluded(specifier) { + var parts = specifier.split(' '); + var op = parts.length > 1 ? parts[0] : '='; + var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); + + for (var i = 0; i < 3; ++i) { + var cur = Number(current[i] || 0); + var ver = Number(versionParts[i] || 0); + if (cur === ver) { + continue; // eslint-disable-line no-restricted-syntax, no-continue + } + if (op === '<') { + return cur < ver; + } else if (op === '>=') { + return cur >= ver; + } else { + return false; + } + } + return op === '>='; +} + +function matchesRange(range) { + var specifiers = range.split(/ ?&& ?/); + if (specifiers.length === 0) { return false; } + for (var i = 0; i < specifiers.length; ++i) { + if (!specifierIncluded(specifiers[i])) { return false; } + } + return true; +} + +function versionIncluded(specifierValue) { + if (typeof specifierValue === 'boolean') { return specifierValue; } + if (specifierValue && typeof specifierValue === 'object') { + for (var i = 0; i < specifierValue.length; ++i) { + if (matchesRange(specifierValue[i])) { return true; } + } + return false; + } + return matchesRange(specifierValue); +} + +var data = __webpack_require__(200); + +var core = {}; +for (var mod in data) { // eslint-disable-line no-restricted-syntax + if (Object.prototype.hasOwnProperty.call(data, mod)) { + core[mod] = versionIncluded(data[mod]); + } +} +module.exports = core; + + /***/ }), /* 200 */ +/***/ (function(module) { + +module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); + +/***/ }), +/* 201 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(193); +var isCore = __webpack_require__(198); var fs = __webpack_require__(132); var path = __webpack_require__(4); -var caller = __webpack_require__(196); -var nodeModulesPaths = __webpack_require__(197); -var normalizeOptions = __webpack_require__(199); +var caller = __webpack_require__(194); +var nodeModulesPaths = __webpack_require__(195); +var normalizeOptions = __webpack_require__(197); + +var realpathFS = fs.realpathSync && typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync; var defaultIsFile = function isFile(file) { try { @@ -20706,7 +20822,43 @@ var defaultIsFile = function isFile(file) { return stat.isFile() || stat.isFIFO(); }; -module.exports = function (x, options) { +var defaultIsDir = function isDirectory(dir) { + try { + var stat = fs.statSync(dir); + } catch (e) { + if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return false; + throw e; + } + return stat.isDirectory(); +}; + +var defaultRealpathSync = function realpathSync(x) { + try { + return realpathFS(x); + } catch (realpathErr) { + if (realpathErr.code !== 'ENOENT') { + throw realpathErr; + } + } + return x; +}; + +var maybeRealpathSync = function maybeRealpathSync(realpathSync, x, opts) { + if (opts && opts.preserveSymlinks === false) { + return realpathSync(x); + } + return x; +}; + +var getPackageCandidates = function getPackageCandidates(x, start, opts) { + var dirs = nodeModulesPaths(start, opts, x); + for (var i = 0; i < dirs.length; i++) { + dirs[i] = path.join(dirs[i], x); + } + return dirs; +}; + +module.exports = function resolveSync(x, options) { if (typeof x !== 'string') { throw new TypeError('Path must be a string.'); } @@ -20714,6 +20866,9 @@ module.exports = function (x, options) { var isFile = opts.isFile || defaultIsFile; var readFileSync = opts.readFileSync || fs.readFileSync; + var isDirectory = opts.isDirectory || defaultIsDir; + var realpathSync = opts.realpathSync || defaultRealpathSync; + var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; var basedir = opts.basedir || path.dirname(caller()); @@ -20722,30 +20877,20 @@ module.exports = function (x, options) { opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory - var absoluteStart = path.resolve(basedir); - - if (opts.preserveSymlinks === false) { - try { - absoluteStart = fs.realpathSync(absoluteStart); - } catch (realPathErr) { - if (realPathErr.code !== 'ENOENT') { - throw realPathErr; - } - } - } + var absoluteStart = maybeRealpathSync(realpathSync, path.resolve(basedir), opts); if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) { var res = path.resolve(absoluteStart, x); - if (x === '..' || x.slice(-1) === '/') res += '/'; + if (x === '.' || x === '..' || x.slice(-1) === '/') res += '/'; var m = loadAsFileSync(res) || loadAsDirectorySync(res); - if (m) return m; + if (m) return maybeRealpathSync(realpathSync, m, opts); + } else if (isCore(x)) { + return x; } else { var n = loadNodeModulesSync(x, absoluteStart); - if (n) return n; + if (n) return maybeRealpathSync(realpathSync, n, opts); } - if (core[x]) return x; - var err = new Error("Cannot find module '" + x + "' from '" + parent + "'"); err.code = 'MODULE_NOT_FOUND'; throw err; @@ -20780,7 +20925,7 @@ module.exports = function (x, options) { } if ((/[/\\]node_modules[/\\]*$/).test(dir)) return; - var pkgfile = path.join(dir, 'package.json'); + var pkgfile = path.join(maybeRealpathSync(realpathSync, dir, opts), 'package.json'); if (!isFile(pkgfile)) { return loadpkg(path.dirname(dir)); @@ -20793,25 +20938,27 @@ module.exports = function (x, options) { } catch (jsonErr) {} if (pkg && opts.packageFilter) { - pkg = opts.packageFilter(pkg, dir); + // v2 will pass pkgfile + pkg = opts.packageFilter(pkg, /*pkgfile,*/ dir); // eslint-disable-line spaced-comment } return { pkg: pkg, dir: dir }; } function loadAsDirectorySync(x) { - var pkgfile = path.join(x, '/package.json'); + var pkgfile = path.join(maybeRealpathSync(realpathSync, x, opts), '/package.json'); if (isFile(pkgfile)) { try { var body = readFileSync(pkgfile, 'UTF8'); var pkg = JSON.parse(body); } catch (e) {} - if (opts.packageFilter) { - pkg = opts.packageFilter(pkg, x); + if (pkg && opts.packageFilter) { + // v2 will pass pkgfile + pkg = opts.packageFilter(pkg, /*pkgfile,*/ x); // eslint-disable-line spaced-comment } - if (pkg.main) { + if (pkg && pkg.main) { if (typeof pkg.main !== 'string') { var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string'); mainError.code = 'INVALID_PACKAGE_MAIN'; @@ -20833,20 +20980,24 @@ module.exports = function (x, options) { } function loadNodeModulesSync(x, start) { - var dirs = nodeModulesPaths(start, opts, x); + var thunk = function () { return getPackageCandidates(x, start, opts); }; + var dirs = packageIterator ? packageIterator(x, start, thunk, opts) : thunk(); + for (var i = 0; i < dirs.length; i++) { var dir = dirs[i]; - var m = loadAsFileSync(path.join(dir, '/', x)); - if (m) return m; - var n = loadAsDirectorySync(path.join(dir, '/', x)); - if (n) return n; + if (isDirectory(path.dirname(dir))) { + var m = loadAsFileSync(dir); + if (m) return m; + var n = loadAsDirectorySync(dir); + if (n) return n; + } } } }; /***/ }), -/* 201 */ +/* 202 */ /***/ (function(module, exports) { module.exports = extractDescription @@ -20866,17 +21017,17 @@ function extractDescription (d) { /***/ }), -/* 202 */ +/* 203 */ /***/ (function(module) { module.exports = JSON.parse("{\"topLevel\":{\"dependancies\":\"dependencies\",\"dependecies\":\"dependencies\",\"depdenencies\":\"dependencies\",\"devEependencies\":\"devDependencies\",\"depends\":\"dependencies\",\"dev-dependencies\":\"devDependencies\",\"devDependences\":\"devDependencies\",\"devDepenencies\":\"devDependencies\",\"devdependencies\":\"devDependencies\",\"repostitory\":\"repository\",\"repo\":\"repository\",\"prefereGlobal\":\"preferGlobal\",\"hompage\":\"homepage\",\"hampage\":\"homepage\",\"autohr\":\"author\",\"autor\":\"author\",\"contributers\":\"contributors\",\"publicationConfig\":\"publishConfig\",\"script\":\"scripts\"},\"bugs\":{\"web\":\"url\",\"name\":\"url\"},\"script\":{\"server\":\"start\",\"tests\":\"test\"}}"); /***/ }), -/* 203 */ +/* 204 */ /***/ (function(module, exports, __webpack_require__) { var util = __webpack_require__(111) -var messages = __webpack_require__(204) +var messages = __webpack_require__(205) module.exports = function() { var args = Array.prototype.slice.call(arguments, 0) @@ -20901,20 +21052,20 @@ function makeTypoWarning (providedName, probableName, field) { /***/ }), -/* 204 */ +/* 205 */ /***/ (function(module) { module.exports = JSON.parse("{\"repositories\":\"'repositories' (plural) Not supported. Please pick one as the 'repository' field\",\"missingRepository\":\"No repository field.\",\"brokenGitUrl\":\"Probably broken git url: %s\",\"nonObjectScripts\":\"scripts must be an object\",\"nonStringScript\":\"script values must be string commands\",\"nonArrayFiles\":\"Invalid 'files' member\",\"invalidFilename\":\"Invalid filename in 'files' list: %s\",\"nonArrayBundleDependencies\":\"Invalid 'bundleDependencies' list. Must be array of package names\",\"nonStringBundleDependency\":\"Invalid bundleDependencies member: %s\",\"nonDependencyBundleDependency\":\"Non-dependency in bundleDependencies: %s\",\"nonObjectDependencies\":\"%s field must be an object\",\"nonStringDependency\":\"Invalid dependency: %s %s\",\"deprecatedArrayDependencies\":\"specifying %s as array is deprecated\",\"deprecatedModules\":\"modules field is deprecated\",\"nonArrayKeywords\":\"keywords should be an array of strings\",\"nonStringKeyword\":\"keywords should be an array of strings\",\"conflictingName\":\"%s is also the name of a node core module.\",\"nonStringDescription\":\"'description' field should be a string\",\"missingDescription\":\"No description\",\"missingReadme\":\"No README data\",\"missingLicense\":\"No license field.\",\"nonEmailUrlBugsString\":\"Bug string field must be url, email, or {email,url}\",\"nonUrlBugsUrlField\":\"bugs.url field must be a string url. Deleted.\",\"nonEmailBugsEmailField\":\"bugs.email field must be a string email. Deleted.\",\"emptyNormalizedBugs\":\"Normalized value of bugs field is an empty object. Deleted.\",\"nonUrlHomepage\":\"homepage field must be a string url. Deleted.\",\"invalidLicense\":\"license should be a valid SPDX license expression\",\"typo\":\"%s should probably be %s.\"}"); /***/ }), -/* 205 */ +/* 206 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const writeJsonFile = __webpack_require__(206); -const sortKeys = __webpack_require__(220); +const writeJsonFile = __webpack_require__(207); +const sortKeys = __webpack_require__(221); const dependencyKeys = new Set([ 'dependencies', @@ -20979,18 +21130,18 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 206 */ +/* 207 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const fs = __webpack_require__(207); -const writeFileAtomic = __webpack_require__(211); -const sortKeys = __webpack_require__(220); -const makeDir = __webpack_require__(222); -const pify = __webpack_require__(223); -const detectIndent = __webpack_require__(224); +const fs = __webpack_require__(208); +const writeFileAtomic = __webpack_require__(212); +const sortKeys = __webpack_require__(221); +const makeDir = __webpack_require__(223); +const pify = __webpack_require__(224); +const detectIndent = __webpack_require__(225); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -21062,13 +21213,13 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 207 */ +/* 208 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(132) -var polyfills = __webpack_require__(208) -var legacy = __webpack_require__(209) -var clone = __webpack_require__(210) +var polyfills = __webpack_require__(209) +var legacy = __webpack_require__(210) +var clone = __webpack_require__(211) var queue = [] @@ -21347,7 +21498,7 @@ function retry () { /***/ }), -/* 208 */ +/* 209 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(134) @@ -21682,7 +21833,7 @@ function patch (fs) { /***/ }), -/* 209 */ +/* 210 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(136).Stream @@ -21806,7 +21957,7 @@ function legacy (fs) { /***/ }), -/* 210 */ +/* 211 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -21832,7 +21983,7 @@ function clone (obj) { /***/ }), -/* 211 */ +/* 212 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -21842,9 +21993,9 @@ module.exports.sync = writeFileSync module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit -var fs = __webpack_require__(212) -var MurmurHash3 = __webpack_require__(216) -var onExit = __webpack_require__(217) +var fs = __webpack_require__(213) +var MurmurHash3 = __webpack_require__(217) +var onExit = __webpack_require__(218) var path = __webpack_require__(4) var activeFiles = {} @@ -21852,7 +22003,7 @@ var activeFiles = {} /* istanbul ignore next */ var threadId = (function getId () { try { - var workerThreads = __webpack_require__(219) + var workerThreads = __webpack_require__(220) /// if we are in main thread, this is set to `0` return workerThreads.threadId @@ -22077,12 +22228,12 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 212 */ +/* 213 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(132) -var polyfills = __webpack_require__(213) -var legacy = __webpack_require__(215) +var polyfills = __webpack_require__(214) +var legacy = __webpack_require__(216) var queue = [] var util = __webpack_require__(111) @@ -22106,7 +22257,7 @@ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { }) } -module.exports = patch(__webpack_require__(214)) +module.exports = patch(__webpack_require__(215)) if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH) { module.exports = patch(fs) } @@ -22345,10 +22496,10 @@ function retry () { /***/ }), -/* 213 */ +/* 214 */ /***/ (function(module, exports, __webpack_require__) { -var fs = __webpack_require__(214) +var fs = __webpack_require__(215) var constants = __webpack_require__(134) var origCwd = process.cwd @@ -22681,7 +22832,7 @@ function chownErOk (er) { /***/ }), -/* 214 */ +/* 215 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -22709,7 +22860,7 @@ function clone (obj) { /***/ }), -/* 215 */ +/* 216 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(136).Stream @@ -22833,7 +22984,7 @@ function legacy (fs) { /***/ }), -/* 216 */ +/* 217 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -22975,14 +23126,14 @@ function legacy (fs) { /***/ }), -/* 217 */ +/* 218 */ /***/ (function(module, exports, __webpack_require__) { // Note: since nyc uses this module to output coverage, any lines // that are in the direct sync flow of nyc's outputCoverage are // ignored, since we can never get coverage for them. var assert = __webpack_require__(138) -var signals = __webpack_require__(218) +var signals = __webpack_require__(219) var EE = __webpack_require__(154) /* istanbul ignore if */ @@ -23138,7 +23289,7 @@ function processEmit (ev, arg) { /***/ }), -/* 218 */ +/* 219 */ /***/ (function(module, exports) { // This is not the set of all possible signals. @@ -23197,18 +23348,18 @@ if (process.platform === 'linux') { /***/ }), -/* 219 */ +/* 220 */ /***/ (function(module, exports) { module.exports = require(undefined); /***/ }), -/* 220 */ +/* 221 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isPlainObj = __webpack_require__(221); +const isPlainObj = __webpack_require__(222); module.exports = (obj, opts) => { if (!isPlainObj(obj)) { @@ -23265,7 +23416,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 221 */ +/* 222 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -23279,14 +23430,14 @@ module.exports = function (x) { /***/ }), -/* 222 */ +/* 223 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); const path = __webpack_require__(4); -const pify = __webpack_require__(223); +const pify = __webpack_require__(224); const semver = __webpack_require__(182); const defaults = { @@ -23425,7 +23576,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 223 */ +/* 224 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -23500,7 +23651,7 @@ module.exports = (input, options) => { /***/ }), -/* 224 */ +/* 225 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -23629,7 +23780,7 @@ module.exports = str => { /***/ }), -/* 225 */ +/* 226 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23638,7 +23789,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnWorkspacesInfo", function() { return yarnWorkspacesInfo; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(227); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -23715,7 +23866,7 @@ async function yarnWorkspacesInfo(directory) { } /***/ }), -/* 226 */ +/* 227 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -23726,9 +23877,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var stream__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(stream__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(112); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(227); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(228); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(262); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(263); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(142); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -23814,23 +23965,23 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 227 */ +/* 228 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const childProcess = __webpack_require__(228); -const crossSpawn = __webpack_require__(229); -const stripFinalNewline = __webpack_require__(242); -const npmRunPath = __webpack_require__(243); -const onetime = __webpack_require__(244); -const makeError = __webpack_require__(246); -const normalizeStdio = __webpack_require__(251); -const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(252); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(253); -const {mergePromise, getSpawnedPromise} = __webpack_require__(260); -const {joinCommand, parseCommand} = __webpack_require__(261); +const childProcess = __webpack_require__(229); +const crossSpawn = __webpack_require__(230); +const stripFinalNewline = __webpack_require__(243); +const npmRunPath = __webpack_require__(244); +const onetime = __webpack_require__(245); +const makeError = __webpack_require__(247); +const normalizeStdio = __webpack_require__(252); +const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(253); +const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(254); +const {mergePromise, getSpawnedPromise} = __webpack_require__(261); +const {joinCommand, parseCommand} = __webpack_require__(262); const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -24077,21 +24228,21 @@ module.exports.node = (scriptPath, args, options = {}) => { /***/ }), -/* 228 */ +/* 229 */ /***/ (function(module, exports) { module.exports = require("child_process"); /***/ }), -/* 229 */ +/* 230 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const cp = __webpack_require__(228); -const parse = __webpack_require__(230); -const enoent = __webpack_require__(241); +const cp = __webpack_require__(229); +const parse = __webpack_require__(231); +const enoent = __webpack_require__(242); function spawn(command, args, options) { // Parse the arguments @@ -24129,16 +24280,16 @@ module.exports._enoent = enoent; /***/ }), -/* 230 */ +/* 231 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const resolveCommand = __webpack_require__(231); -const escape = __webpack_require__(237); -const readShebang = __webpack_require__(238); +const resolveCommand = __webpack_require__(232); +const escape = __webpack_require__(238); +const readShebang = __webpack_require__(239); const isWin = process.platform === 'win32'; const isExecutableRegExp = /\.(?:com|exe)$/i; @@ -24227,15 +24378,15 @@ module.exports = parse; /***/ }), -/* 231 */ +/* 232 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const which = __webpack_require__(232); -const pathKey = __webpack_require__(236)(); +const which = __webpack_require__(233); +const pathKey = __webpack_require__(237)(); function resolveCommandAttempt(parsed, withoutPathExt) { const cwd = process.cwd(); @@ -24285,7 +24436,7 @@ module.exports = resolveCommand; /***/ }), -/* 232 */ +/* 233 */ /***/ (function(module, exports, __webpack_require__) { const isWindows = process.platform === 'win32' || @@ -24294,7 +24445,7 @@ const isWindows = process.platform === 'win32' || const path = __webpack_require__(4) const COLON = isWindows ? ';' : ':' -const isexe = __webpack_require__(233) +const isexe = __webpack_require__(234) const getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) @@ -24416,15 +24567,15 @@ which.sync = whichSync /***/ }), -/* 233 */ +/* 234 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(132) var core if (process.platform === 'win32' || global.TESTING_WINDOWS) { - core = __webpack_require__(234) -} else { core = __webpack_require__(235) +} else { + core = __webpack_require__(236) } module.exports = isexe @@ -24479,7 +24630,7 @@ function sync (path, options) { /***/ }), -/* 234 */ +/* 235 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -24527,7 +24678,7 @@ function sync (path, options) { /***/ }), -/* 235 */ +/* 236 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -24574,7 +24725,7 @@ function checkMode (stat, options) { /***/ }), -/* 236 */ +/* 237 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24597,7 +24748,7 @@ module.exports.default = pathKey; /***/ }), -/* 237 */ +/* 238 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24649,14 +24800,14 @@ module.exports.argument = escapeArgument; /***/ }), -/* 238 */ +/* 239 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const shebangCommand = __webpack_require__(239); +const shebangCommand = __webpack_require__(240); function readShebang(command) { // Read the first 150 bytes from the file @@ -24679,12 +24830,12 @@ module.exports = readShebang; /***/ }), -/* 239 */ +/* 240 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const shebangRegex = __webpack_require__(240); +const shebangRegex = __webpack_require__(241); module.exports = (string = '') => { const match = string.match(shebangRegex); @@ -24705,7 +24856,7 @@ module.exports = (string = '') => { /***/ }), -/* 240 */ +/* 241 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24714,7 +24865,7 @@ module.exports = /^#!(.*)/; /***/ }), -/* 241 */ +/* 242 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24780,7 +24931,7 @@ module.exports = { /***/ }), -/* 242 */ +/* 243 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24803,13 +24954,13 @@ module.exports = input => { /***/ }), -/* 243 */ +/* 244 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathKey = __webpack_require__(236); +const pathKey = __webpack_require__(237); const npmRunPath = options => { options = { @@ -24857,12 +25008,12 @@ module.exports.env = options => { /***/ }), -/* 244 */ +/* 245 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(245); +const mimicFn = __webpack_require__(246); const calledFunctions = new WeakMap(); @@ -24914,7 +25065,7 @@ module.exports.callCount = fn => { /***/ }), -/* 245 */ +/* 246 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -24934,12 +25085,12 @@ module.exports.default = mimicFn; /***/ }), -/* 246 */ +/* 247 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const {signalsByName} = __webpack_require__(247); +const {signalsByName} = __webpack_require__(248); const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { if (timedOut) { @@ -25027,14 +25178,14 @@ module.exports = makeError; /***/ }), -/* 247 */ +/* 248 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.signalsByNumber=exports.signalsByName=void 0;var _os=__webpack_require__(121); -var _signals=__webpack_require__(248); -var _realtime=__webpack_require__(250); +var _signals=__webpack_require__(249); +var _realtime=__webpack_require__(251); @@ -25104,14 +25255,14 @@ const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumb //# sourceMappingURL=main.js.map /***/ }), -/* 248 */ +/* 249 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.getSignals=void 0;var _os=__webpack_require__(121); -var _core=__webpack_require__(249); -var _realtime=__webpack_require__(250); +var _core=__webpack_require__(250); +var _realtime=__webpack_require__(251); @@ -25145,7 +25296,7 @@ return{name,number,description,supported,action,forced,standard}; //# sourceMappingURL=signals.js.map /***/ }), -/* 249 */ +/* 250 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -25424,7 +25575,7 @@ standard:"other"}];exports.SIGNALS=SIGNALS; //# sourceMappingURL=core.js.map /***/ }), -/* 250 */ +/* 251 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -25449,7 +25600,7 @@ const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; //# sourceMappingURL=realtime.js.map /***/ }), -/* 251 */ +/* 252 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -25508,13 +25659,13 @@ module.exports.node = opts => { /***/ }), -/* 252 */ +/* 253 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const os = __webpack_require__(121); -const onExit = __webpack_require__(217); +const onExit = __webpack_require__(218); const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; @@ -25627,14 +25778,14 @@ module.exports = { /***/ }), -/* 253 */ +/* 254 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isStream = __webpack_require__(254); -const getStream = __webpack_require__(255); -const mergeStream = __webpack_require__(259); +const isStream = __webpack_require__(255); +const getStream = __webpack_require__(256); +const mergeStream = __webpack_require__(260); // `input` option const handleInput = (spawned, input) => { @@ -25731,7 +25882,7 @@ module.exports = { /***/ }), -/* 254 */ +/* 255 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -25767,13 +25918,13 @@ module.exports = isStream; /***/ }), -/* 255 */ +/* 256 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pump = __webpack_require__(256); -const bufferStream = __webpack_require__(258); +const pump = __webpack_require__(257); +const bufferStream = __webpack_require__(259); class MaxBufferError extends Error { constructor() { @@ -25832,11 +25983,11 @@ module.exports.MaxBufferError = MaxBufferError; /***/ }), -/* 256 */ +/* 257 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(160) -var eos = __webpack_require__(257) +var eos = __webpack_require__(258) var fs = __webpack_require__(132) // we only need fs to get the ReadStream and WriteStream prototypes var noop = function () {} @@ -25920,7 +26071,7 @@ module.exports = pump /***/ }), -/* 257 */ +/* 258 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(160); @@ -26013,7 +26164,7 @@ module.exports = eos; /***/ }), -/* 258 */ +/* 259 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26072,7 +26223,7 @@ module.exports = options => { /***/ }), -/* 259 */ +/* 260 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26120,7 +26271,7 @@ module.exports = function (/*streams...*/) { /***/ }), -/* 260 */ +/* 261 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26173,7 +26324,7 @@ module.exports = { /***/ }), -/* 261 */ +/* 262 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26218,7 +26369,7 @@ module.exports = { /***/ }), -/* 262 */ +/* 263 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -26226,12 +26377,12 @@ module.exports = { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(263); -module.exports.cli = __webpack_require__(267); +module.exports = __webpack_require__(264); +module.exports.cli = __webpack_require__(268); /***/ }), -/* 263 */ +/* 264 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26246,9 +26397,9 @@ var stream = __webpack_require__(136); var util = __webpack_require__(111); var fs = __webpack_require__(132); -var through = __webpack_require__(264); -var duplexer = __webpack_require__(265); -var StringDecoder = __webpack_require__(266).StringDecoder; +var through = __webpack_require__(265); +var duplexer = __webpack_require__(266); +var StringDecoder = __webpack_require__(267).StringDecoder; module.exports = Logger; @@ -26437,7 +26588,7 @@ function lineMerger(host) { /***/ }), -/* 264 */ +/* 265 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(136) @@ -26551,7 +26702,7 @@ function through (write, end, opts) { /***/ }), -/* 265 */ +/* 266 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(136) @@ -26644,13 +26795,13 @@ function duplex(writer, reader) { /***/ }), -/* 266 */ +/* 267 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 267 */ +/* 268 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -26661,11 +26812,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(268); +var minimist = __webpack_require__(269); var path = __webpack_require__(4); -var Logger = __webpack_require__(263); -var pkg = __webpack_require__(269); +var Logger = __webpack_require__(264); +var pkg = __webpack_require__(270); module.exports = cli; @@ -26719,7 +26870,7 @@ function usage($0, p) { /***/ }), -/* 268 */ +/* 269 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -26961,13 +27112,13 @@ function isNumber (x) { /***/ }), -/* 269 */ +/* 270 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 270 */ +/* 271 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -26980,7 +27131,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(111); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(271); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(272); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(129); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(163); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(144); @@ -27075,7 +27226,7 @@ function packagesFromGlobPattern({ } /***/ }), -/* 271 */ +/* 272 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -27145,7 +27296,7 @@ function getProjectPaths({ } /***/ }), -/* 272 */ +/* 273 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -27153,13 +27304,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(132); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(273); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(274); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(111); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(227); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(228); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(274); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(275); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -27388,19 +27539,19 @@ async function getAllChecksums(kbn, log) { } /***/ }), -/* 273 */ +/* 274 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 274 */ +/* 275 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(275); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(276); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(129); /* @@ -27444,7 +27595,7 @@ async function readYarnLock(kbn) { } /***/ }), -/* 275 */ +/* 276 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -29003,7 +29154,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(273); +module.exports = __webpack_require__(274); /***/ }), /* 10 */, @@ -31327,7 +31478,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(276); +module.exports = __webpack_require__(277); /***/ }), /* 64 */, @@ -32265,7 +32416,7 @@ module.exports.win32 = win32; /* 79 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(277); +module.exports = __webpack_require__(278); /***/ }), /* 80 */, @@ -37722,19 +37873,19 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 276 */ +/* 277 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 277 */ +/* 278 */ /***/ (function(module, exports) { module.exports = require("tty"); /***/ }), -/* 278 */ +/* 279 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -37831,15 +37982,15 @@ class BootstrapCacheFile { } /***/ }), -/* 279 */ +/* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(280); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(281); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(372); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(373); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -37939,21 +38090,21 @@ const CleanCommand = { }; /***/ }), -/* 280 */ +/* 281 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(111); const path = __webpack_require__(4); -const globby = __webpack_require__(281); -const isGlob = __webpack_require__(364); -const slash = __webpack_require__(362); +const globby = __webpack_require__(282); +const isGlob = __webpack_require__(365); +const slash = __webpack_require__(363); const gracefulFs = __webpack_require__(131); -const isPathCwd = __webpack_require__(365); -const isPathInside = __webpack_require__(366); -const rimraf = __webpack_require__(367); -const pMap = __webpack_require__(368); +const isPathCwd = __webpack_require__(366); +const isPathInside = __webpack_require__(367); +const rimraf = __webpack_require__(368); +const pMap = __webpack_require__(369); const rimrafP = promisify(rimraf); @@ -38067,19 +38218,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 281 */ +/* 282 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const arrayUnion = __webpack_require__(282); -const merge2 = __webpack_require__(283); -const glob = __webpack_require__(284); -const fastGlob = __webpack_require__(289); -const dirGlob = __webpack_require__(358); -const gitignore = __webpack_require__(360); -const {FilterStream, UniqueStream} = __webpack_require__(363); +const arrayUnion = __webpack_require__(283); +const merge2 = __webpack_require__(284); +const glob = __webpack_require__(285); +const fastGlob = __webpack_require__(290); +const dirGlob = __webpack_require__(359); +const gitignore = __webpack_require__(361); +const {FilterStream, UniqueStream} = __webpack_require__(364); const DEFAULT_FILTER = () => false; @@ -38252,7 +38403,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 282 */ +/* 283 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38264,7 +38415,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 283 */ +/* 284 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38415,7 +38566,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 284 */ +/* 285 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -38464,13 +38615,13 @@ var fs = __webpack_require__(132) var rp = __webpack_require__(146) var minimatch = __webpack_require__(148) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(285) +var inherits = __webpack_require__(286) var EE = __webpack_require__(154).EventEmitter var path = __webpack_require__(4) var assert = __webpack_require__(138) var isAbsolute = __webpack_require__(155) -var globSync = __webpack_require__(287) -var common = __webpack_require__(288) +var globSync = __webpack_require__(288) +var common = __webpack_require__(289) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -39211,7 +39362,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 285 */ +/* 286 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -39221,12 +39372,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(286); + module.exports = __webpack_require__(287); } /***/ }), -/* 286 */ +/* 287 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -39259,7 +39410,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 287 */ +/* 288 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -39269,12 +39420,12 @@ var fs = __webpack_require__(132) var rp = __webpack_require__(146) var minimatch = __webpack_require__(148) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(284).Glob +var Glob = __webpack_require__(285).Glob var util = __webpack_require__(111) var path = __webpack_require__(4) var assert = __webpack_require__(138) var isAbsolute = __webpack_require__(155) -var common = __webpack_require__(288) +var common = __webpack_require__(289) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -39751,7 +39902,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 288 */ +/* 289 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -39997,17 +40148,17 @@ function childrenIgnored (self, path) { /***/ }), -/* 289 */ +/* 290 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(290); -const async_1 = __webpack_require__(319); -const stream_1 = __webpack_require__(354); -const sync_1 = __webpack_require__(355); -const settings_1 = __webpack_require__(357); -const utils = __webpack_require__(291); +const taskManager = __webpack_require__(291); +const async_1 = __webpack_require__(320); +const stream_1 = __webpack_require__(355); +const sync_1 = __webpack_require__(356); +const settings_1 = __webpack_require__(358); +const utils = __webpack_require__(292); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -40071,13 +40222,13 @@ module.exports = FastGlob; /***/ }), -/* 290 */ +/* 291 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); +const utils = __webpack_require__(292); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -40142,30 +40293,30 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 291 */ +/* 292 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(292); +const array = __webpack_require__(293); exports.array = array; -const errno = __webpack_require__(293); +const errno = __webpack_require__(294); exports.errno = errno; -const fs = __webpack_require__(294); +const fs = __webpack_require__(295); exports.fs = fs; -const path = __webpack_require__(295); +const path = __webpack_require__(296); exports.path = path; -const pattern = __webpack_require__(296); +const pattern = __webpack_require__(297); exports.pattern = pattern; -const stream = __webpack_require__(317); +const stream = __webpack_require__(318); exports.stream = stream; -const string = __webpack_require__(318); +const string = __webpack_require__(319); exports.string = string; /***/ }), -/* 292 */ +/* 293 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40193,7 +40344,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 293 */ +/* 294 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40206,7 +40357,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 294 */ +/* 295 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40231,7 +40382,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 295 */ +/* 296 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40270,16 +40421,16 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 296 */ +/* 297 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const globParent = __webpack_require__(297); -const micromatch = __webpack_require__(300); -const picomatch = __webpack_require__(311); +const globParent = __webpack_require__(298); +const micromatch = __webpack_require__(301); +const picomatch = __webpack_require__(312); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; @@ -40389,13 +40540,13 @@ exports.matchAny = matchAny; /***/ }), -/* 297 */ +/* 298 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(298); +var isGlob = __webpack_require__(299); var pathPosixDirname = __webpack_require__(4).posix.dirname; var isWin32 = __webpack_require__(121).platform() === 'win32'; @@ -40437,7 +40588,7 @@ module.exports = function globParent(str, opts) { /***/ }), -/* 298 */ +/* 299 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -40447,7 +40598,7 @@ module.exports = function globParent(str, opts) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(299); +var isExtglob = __webpack_require__(300); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -40491,7 +40642,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 299 */ +/* 300 */ /***/ (function(module, exports) { /*! @@ -40517,16 +40668,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 300 */ +/* 301 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(111); -const braces = __webpack_require__(301); -const picomatch = __webpack_require__(311); -const utils = __webpack_require__(314); +const braces = __webpack_require__(302); +const picomatch = __webpack_require__(312); +const utils = __webpack_require__(315); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -40991,16 +41142,16 @@ module.exports = micromatch; /***/ }), -/* 301 */ +/* 302 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(302); -const compile = __webpack_require__(304); -const expand = __webpack_require__(308); -const parse = __webpack_require__(309); +const stringify = __webpack_require__(303); +const compile = __webpack_require__(305); +const expand = __webpack_require__(309); +const parse = __webpack_require__(310); /** * Expand the given pattern or create a regex-compatible string. @@ -41168,13 +41319,13 @@ module.exports = braces; /***/ }), -/* 302 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(303); +const utils = __webpack_require__(304); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -41207,7 +41358,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 303 */ +/* 304 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41326,14 +41477,14 @@ exports.flatten = (...args) => { /***/ }), -/* 304 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(305); -const utils = __webpack_require__(303); +const fill = __webpack_require__(306); +const utils = __webpack_require__(304); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -41390,7 +41541,7 @@ module.exports = compile; /***/ }), -/* 305 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41404,7 +41555,7 @@ module.exports = compile; const util = __webpack_require__(111); -const toRegexRange = __webpack_require__(306); +const toRegexRange = __webpack_require__(307); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -41646,7 +41797,7 @@ module.exports = fill; /***/ }), -/* 306 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41659,7 +41810,7 @@ module.exports = fill; -const isNumber = __webpack_require__(307); +const isNumber = __webpack_require__(308); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -41941,7 +42092,7 @@ module.exports = toRegexRange; /***/ }), -/* 307 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41966,15 +42117,15 @@ module.exports = function(num) { /***/ }), -/* 308 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(305); -const stringify = __webpack_require__(302); -const utils = __webpack_require__(303); +const fill = __webpack_require__(306); +const stringify = __webpack_require__(303); +const utils = __webpack_require__(304); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -42086,13 +42237,13 @@ module.exports = expand; /***/ }), -/* 309 */ +/* 310 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(302); +const stringify = __webpack_require__(303); /** * Constants @@ -42114,7 +42265,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(310); +} = __webpack_require__(311); /** * parse @@ -42426,7 +42577,7 @@ module.exports = parse; /***/ }), -/* 310 */ +/* 311 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42490,27 +42641,27 @@ module.exports = { /***/ }), -/* 311 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(312); +module.exports = __webpack_require__(313); /***/ }), -/* 312 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(313); -const parse = __webpack_require__(316); -const utils = __webpack_require__(314); -const constants = __webpack_require__(315); +const scan = __webpack_require__(314); +const parse = __webpack_require__(317); +const utils = __webpack_require__(315); +const constants = __webpack_require__(316); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -42846,13 +42997,13 @@ module.exports = picomatch; /***/ }), -/* 313 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(314); +const utils = __webpack_require__(315); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -42869,7 +43020,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(315); +} = __webpack_require__(316); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -43236,7 +43387,7 @@ module.exports = scan; /***/ }), -/* 314 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43249,7 +43400,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(315); +} = __webpack_require__(316); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -43307,7 +43458,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 315 */ +/* 316 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43493,14 +43644,14 @@ module.exports = { /***/ }), -/* 316 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(315); -const utils = __webpack_require__(314); +const constants = __webpack_require__(316); +const utils = __webpack_require__(315); /** * Constants @@ -44578,13 +44729,13 @@ module.exports = parse; /***/ }), -/* 317 */ +/* 318 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(283); +const merge2 = __webpack_require__(284); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -44601,7 +44752,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 318 */ +/* 319 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44618,14 +44769,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 319 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(320); -const provider_1 = __webpack_require__(347); +const stream_1 = __webpack_require__(321); +const provider_1 = __webpack_require__(348); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -44653,16 +44804,16 @@ exports.default = ProviderAsync; /***/ }), -/* 320 */ +/* 321 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(136); -const fsStat = __webpack_require__(321); -const fsWalk = __webpack_require__(326); -const reader_1 = __webpack_require__(346); +const fsStat = __webpack_require__(322); +const fsWalk = __webpack_require__(327); +const reader_1 = __webpack_require__(347); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -44715,15 +44866,15 @@ exports.default = ReaderStream; /***/ }), -/* 321 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(322); -const sync = __webpack_require__(323); -const settings_1 = __webpack_require__(324); +const async = __webpack_require__(323); +const sync = __webpack_require__(324); +const settings_1 = __webpack_require__(325); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -44746,7 +44897,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 322 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44784,7 +44935,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 323 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44813,13 +44964,13 @@ exports.read = read; /***/ }), -/* 324 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(325); +const fs = __webpack_require__(326); class Settings { constructor(_options = {}) { this._options = _options; @@ -44836,7 +44987,7 @@ exports.default = Settings; /***/ }), -/* 325 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44859,16 +45010,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 326 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(327); -const stream_1 = __webpack_require__(342); -const sync_1 = __webpack_require__(343); -const settings_1 = __webpack_require__(345); +const async_1 = __webpack_require__(328); +const stream_1 = __webpack_require__(343); +const sync_1 = __webpack_require__(344); +const settings_1 = __webpack_require__(346); exports.Settings = settings_1.default; function walk(directory, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -44898,13 +45049,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 327 */ +/* 328 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(328); +const async_1 = __webpack_require__(329); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -44935,17 +45086,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 328 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(154); -const fsScandir = __webpack_require__(329); -const fastq = __webpack_require__(338); -const common = __webpack_require__(340); -const reader_1 = __webpack_require__(341); +const fsScandir = __webpack_require__(330); +const fastq = __webpack_require__(339); +const common = __webpack_require__(341); +const reader_1 = __webpack_require__(342); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -45035,15 +45186,15 @@ exports.default = AsyncReader; /***/ }), -/* 329 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(330); -const sync = __webpack_require__(335); -const settings_1 = __webpack_require__(336); +const async = __webpack_require__(331); +const sync = __webpack_require__(336); +const settings_1 = __webpack_require__(337); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -45066,16 +45217,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 330 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(321); -const rpl = __webpack_require__(331); -const constants_1 = __webpack_require__(332); -const utils = __webpack_require__(333); +const fsStat = __webpack_require__(322); +const rpl = __webpack_require__(332); +const constants_1 = __webpack_require__(333); +const utils = __webpack_require__(334); function read(directory, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings, callback); @@ -45163,7 +45314,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 331 */ +/* 332 */ /***/ (function(module, exports) { module.exports = runParallel @@ -45217,7 +45368,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 332 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45237,18 +45388,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = IS_MATCHED_BY_MAJOR || IS_MATCHED_B /***/ }), -/* 333 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(334); +const fs = __webpack_require__(335); exports.fs = fs; /***/ }), -/* 334 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45273,15 +45424,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 335 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(321); -const constants_1 = __webpack_require__(332); -const utils = __webpack_require__(333); +const fsStat = __webpack_require__(322); +const constants_1 = __webpack_require__(333); +const utils = __webpack_require__(334); function read(directory, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings); @@ -45332,15 +45483,15 @@ exports.readdir = readdir; /***/ }), -/* 336 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(321); -const fs = __webpack_require__(337); +const fsStat = __webpack_require__(322); +const fs = __webpack_require__(338); class Settings { constructor(_options = {}) { this._options = _options; @@ -45363,7 +45514,7 @@ exports.default = Settings; /***/ }), -/* 337 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45388,13 +45539,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 338 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(339) +var reusify = __webpack_require__(340) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -45568,7 +45719,7 @@ module.exports = fastqueue /***/ }), -/* 339 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45608,7 +45759,7 @@ module.exports = reusify /***/ }), -/* 340 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45639,13 +45790,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 341 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(340); +const common = __webpack_require__(341); class Reader { constructor(_root, _settings) { this._root = _root; @@ -45657,14 +45808,14 @@ exports.default = Reader; /***/ }), -/* 342 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(136); -const async_1 = __webpack_require__(328); +const async_1 = __webpack_require__(329); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -45694,13 +45845,13 @@ exports.default = StreamProvider; /***/ }), -/* 343 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(344); +const sync_1 = __webpack_require__(345); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -45715,15 +45866,15 @@ exports.default = SyncProvider; /***/ }), -/* 344 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(329); -const common = __webpack_require__(340); -const reader_1 = __webpack_require__(341); +const fsScandir = __webpack_require__(330); +const common = __webpack_require__(341); +const reader_1 = __webpack_require__(342); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -45781,14 +45932,14 @@ exports.default = SyncReader; /***/ }), -/* 345 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsScandir = __webpack_require__(329); +const fsScandir = __webpack_require__(330); class Settings { constructor(_options = {}) { this._options = _options; @@ -45814,15 +45965,15 @@ exports.default = Settings; /***/ }), -/* 346 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(321); -const utils = __webpack_require__(291); +const fsStat = __webpack_require__(322); +const utils = __webpack_require__(292); class Reader { constructor(_settings) { this._settings = _settings; @@ -45854,17 +46005,17 @@ exports.default = Reader; /***/ }), -/* 347 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(348); -const entry_1 = __webpack_require__(351); -const error_1 = __webpack_require__(352); -const entry_2 = __webpack_require__(353); +const deep_1 = __webpack_require__(349); +const entry_1 = __webpack_require__(352); +const error_1 = __webpack_require__(353); +const entry_2 = __webpack_require__(354); class Provider { constructor(_settings) { this._settings = _settings; @@ -45909,14 +46060,14 @@ exports.default = Provider; /***/ }), -/* 348 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); -const partial_1 = __webpack_require__(349); +const utils = __webpack_require__(292); +const partial_1 = __webpack_require__(350); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -45970,13 +46121,13 @@ exports.default = DeepFilter; /***/ }), -/* 349 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(350); +const matcher_1 = __webpack_require__(351); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -46015,13 +46166,13 @@ exports.default = PartialMatcher; /***/ }), -/* 350 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); +const utils = __webpack_require__(292); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -46072,13 +46223,13 @@ exports.default = Matcher; /***/ }), -/* 351 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); +const utils = __webpack_require__(292); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -46134,13 +46285,13 @@ exports.default = EntryFilter; /***/ }), -/* 352 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); +const utils = __webpack_require__(292); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -46156,13 +46307,13 @@ exports.default = ErrorFilter; /***/ }), -/* 353 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(291); +const utils = __webpack_require__(292); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -46189,15 +46340,15 @@ exports.default = EntryTransformer; /***/ }), -/* 354 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(136); -const stream_2 = __webpack_require__(320); -const provider_1 = __webpack_require__(347); +const stream_2 = __webpack_require__(321); +const provider_1 = __webpack_require__(348); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -46227,14 +46378,14 @@ exports.default = ProviderStream; /***/ }), -/* 355 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(356); -const provider_1 = __webpack_require__(347); +const sync_1 = __webpack_require__(357); +const provider_1 = __webpack_require__(348); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -46257,15 +46408,15 @@ exports.default = ProviderSync; /***/ }), -/* 356 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(321); -const fsWalk = __webpack_require__(326); -const reader_1 = __webpack_require__(346); +const fsStat = __webpack_require__(322); +const fsWalk = __webpack_require__(327); +const reader_1 = __webpack_require__(347); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -46307,7 +46458,7 @@ exports.default = ReaderSync; /***/ }), -/* 357 */ +/* 358 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46366,13 +46517,13 @@ exports.default = Settings; /***/ }), -/* 358 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(359); +const pathType = __webpack_require__(360); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -46448,7 +46599,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 359 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46498,7 +46649,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 360 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46506,9 +46657,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(111); const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(289); -const gitIgnore = __webpack_require__(361); -const slash = __webpack_require__(362); +const fastGlob = __webpack_require__(290); +const gitIgnore = __webpack_require__(362); +const slash = __webpack_require__(363); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -46622,7 +46773,7 @@ module.exports.sync = options => { /***/ }), -/* 361 */ +/* 362 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -47225,7 +47376,7 @@ if ( /***/ }), -/* 362 */ +/* 363 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47243,7 +47394,7 @@ module.exports = path => { /***/ }), -/* 363 */ +/* 364 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47296,7 +47447,7 @@ module.exports = { /***/ }), -/* 364 */ +/* 365 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -47306,7 +47457,7 @@ module.exports = { * Released under the MIT License. */ -var isExtglob = __webpack_require__(299); +var isExtglob = __webpack_require__(300); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -47350,7 +47501,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 365 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47372,7 +47523,7 @@ module.exports = path_ => { /***/ }), -/* 366 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47400,7 +47551,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 367 */ +/* 368 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(138) @@ -47408,7 +47559,7 @@ const path = __webpack_require__(4) const fs = __webpack_require__(132) let glob = undefined try { - glob = __webpack_require__(284) + glob = __webpack_require__(285) } catch (_err) { // treat glob as optional. } @@ -47774,12 +47925,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 368 */ +/* 369 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(369); +const AggregateError = __webpack_require__(370); module.exports = async ( iterable, @@ -47862,13 +48013,13 @@ module.exports = async ( /***/ }), -/* 369 */ +/* 370 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(370); -const cleanStack = __webpack_require__(371); +const indentString = __webpack_require__(371); +const cleanStack = __webpack_require__(372); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -47916,7 +48067,7 @@ module.exports = AggregateError; /***/ }), -/* 370 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47958,7 +48109,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 371 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48005,15 +48156,15 @@ module.exports = (stack, options) => { /***/ }), -/* 372 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(373); -const cliCursor = __webpack_require__(377); -const cliSpinners = __webpack_require__(381); -const logSymbols = __webpack_require__(383); +const chalk = __webpack_require__(374); +const cliCursor = __webpack_require__(378); +const cliSpinners = __webpack_require__(382); +const logSymbols = __webpack_require__(384); class Ora { constructor(options) { @@ -48160,16 +48311,16 @@ module.exports.promise = (action, options) => { /***/ }), -/* 373 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(113); -const ansiStyles = __webpack_require__(374); -const stdoutColor = __webpack_require__(375).stdout; +const ansiStyles = __webpack_require__(375); +const stdoutColor = __webpack_require__(376).stdout; -const template = __webpack_require__(376); +const template = __webpack_require__(377); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -48395,7 +48546,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 374 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48568,7 +48719,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 375 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48710,7 +48861,7 @@ module.exports = { /***/ }), -/* 376 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48845,12 +48996,12 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 377 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(378); +const restoreCursor = __webpack_require__(379); let hidden = false; @@ -48891,13 +49042,13 @@ exports.toggle = (force, stream) => { /***/ }), -/* 378 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(379); -const signalExit = __webpack_require__(217); +const onetime = __webpack_require__(380); +const signalExit = __webpack_require__(218); module.exports = onetime(() => { signalExit(() => { @@ -48907,12 +49058,12 @@ module.exports = onetime(() => { /***/ }), -/* 379 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(380); +const mimicFn = __webpack_require__(381); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -48953,7 +49104,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 380 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48969,27 +49120,27 @@ module.exports = (to, from) => { /***/ }), -/* 381 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(382); +module.exports = __webpack_require__(383); /***/ }), -/* 382 */ +/* 383 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]}}"); /***/ }), -/* 383 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(384); +const chalk = __webpack_require__(385); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -49011,16 +49162,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 384 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(113); -const ansiStyles = __webpack_require__(385); -const stdoutColor = __webpack_require__(386).stdout; +const ansiStyles = __webpack_require__(386); +const stdoutColor = __webpack_require__(387).stdout; -const template = __webpack_require__(387); +const template = __webpack_require__(388); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -49246,7 +49397,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 385 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49419,7 +49570,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 386 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49561,7 +49712,7 @@ module.exports = { /***/ }), -/* 387 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49696,7 +49847,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 388 */ +/* 389 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -49757,7 +49908,7 @@ const RunCommand = { }; /***/ }), -/* 389 */ +/* 390 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -49767,7 +49918,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(142); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(143); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(390); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(391); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -49852,14 +50003,14 @@ const WatchCommand = { }; /***/ }), -/* 390 */ +/* 391 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(391); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(392); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -49926,141 +50077,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 391 */ +/* 392 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(392); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(393); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(393); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(394); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(394); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(395); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(395); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(396); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(396); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(397); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(397); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(398); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(398); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(399); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(399); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(400); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(400); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(401); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(401); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(402); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(402); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(403); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(403); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(404); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(404); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(405); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(405); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(421); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(422); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(104); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -50071,175 +50222,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(478); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(479); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -50351,7 +50502,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 392 */ +/* 393 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50432,14 +50583,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 393 */ +/* 394 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(392); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(393); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(107); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -50455,7 +50606,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 394 */ +/* 395 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50504,7 +50655,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 395 */ +/* 396 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50605,7 +50756,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 396 */ +/* 397 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50766,7 +50917,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 397 */ +/* 398 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50886,7 +51037,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 398 */ +/* 399 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -50981,7 +51132,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 399 */ +/* 400 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51042,7 +51193,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 400 */ +/* 401 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51058,7 +51209,7 @@ function combineAll(project) { /***/ }), -/* 401 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51090,7 +51241,7 @@ function combineLatest() { /***/ }), -/* 402 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51110,7 +51261,7 @@ function concat() { /***/ }), -/* 403 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51126,13 +51277,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 404 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(403); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(404); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -51142,7 +51293,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 405 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51207,7 +51358,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51295,7 +51446,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 407 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51371,7 +51522,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 408 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51421,7 +51572,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51429,7 +51580,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(410); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(411); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -51528,7 +51679,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 410 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51542,7 +51693,7 @@ function isDate(value) { /***/ }), -/* 411 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51688,7 +51839,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 412 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51726,7 +51877,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 413 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51804,7 +51955,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 414 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51875,13 +52026,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 415 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(414); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(415); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -51891,7 +52042,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 416 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51899,9 +52050,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(104); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(417); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(408); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(418); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(409); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -51923,7 +52074,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 417 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51989,7 +52140,7 @@ function defaultErrorFactory() { /***/ }), -/* 418 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52051,7 +52202,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 419 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52073,7 +52224,7 @@ function endWith() { /***/ }), -/* 420 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52135,7 +52286,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 421 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52192,7 +52343,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 422 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52289,7 +52440,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52408,7 +52559,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52446,7 +52597,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52518,13 +52669,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 426 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(425); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(426); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -52534,7 +52685,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 427 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52542,9 +52693,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(104); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(418); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(408); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(417); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(419); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(409); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(60); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -52561,7 +52712,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 428 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52598,7 +52749,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 429 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52642,7 +52793,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 430 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52650,9 +52801,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(104); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(431); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(417); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(408); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(432); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(418); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(409); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(60); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -52669,7 +52820,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 431 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52746,7 +52897,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52785,7 +52936,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52835,13 +52986,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(435); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -52854,15 +53005,15 @@ function max(comparer) { /***/ }), -/* 435 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(431); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(408); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(437); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(432); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(409); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -52883,7 +53034,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 436 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52965,7 +53116,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52985,7 +53136,7 @@ function merge() { /***/ }), -/* 438 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53010,7 +53161,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 439 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53122,13 +53273,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(435); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -53141,7 +53292,7 @@ function min(comparer) { /***/ }), -/* 441 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53190,7 +53341,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 442 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53281,7 +53432,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53329,7 +53480,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53352,7 +53503,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 445 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53392,14 +53543,14 @@ function plucker(props, length) { /***/ }), -/* 446 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -53412,14 +53563,14 @@ function publish(selector) { /***/ }), -/* 447 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -53430,14 +53581,14 @@ function publishBehavior(value) { /***/ }), -/* 448 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -53448,14 +53599,14 @@ function publishLast() { /***/ }), -/* 449 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -53471,7 +53622,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 450 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53498,7 +53649,7 @@ function race() { /***/ }), -/* 451 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53563,7 +53714,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 452 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53659,7 +53810,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 453 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53712,7 +53863,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53800,7 +53951,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 455 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53857,7 +54008,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 456 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53917,7 +54068,7 @@ function dispatchNotification(state) { /***/ }), -/* 457 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54040,13 +54191,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 458 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(441); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(442); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -54063,7 +54214,7 @@ function share() { /***/ }), -/* 459 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54127,7 +54278,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 460 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54207,7 +54358,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 461 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54249,7 +54400,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 462 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54311,7 +54462,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54368,7 +54519,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54424,7 +54575,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54453,13 +54604,13 @@ function startWith() { /***/ }), -/* 466 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(467); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(468); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -54484,7 +54635,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 467 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54548,13 +54699,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 468 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(469); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(470); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(60); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -54566,7 +54717,7 @@ function switchAll() { /***/ }), -/* 469 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54657,13 +54808,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 470 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(469); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(470); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -54673,7 +54824,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 471 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54723,7 +54874,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54791,7 +54942,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54879,7 +55030,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54983,7 +55134,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54992,7 +55143,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(474); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(475); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -55081,7 +55232,7 @@ function dispatchNext(arg) { /***/ }), -/* 476 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55089,7 +55240,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(436); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(437); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(90); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -55125,7 +55276,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 477 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55133,7 +55284,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(478); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(479); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -55150,7 +55301,7 @@ function timeout(due, scheduler) { /***/ }), -/* 478 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55158,7 +55309,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(410); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(411); /* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(69); /* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(70); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ @@ -55232,7 +55383,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55262,13 +55413,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(435); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -55285,7 +55436,7 @@ function toArray() { /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55365,7 +55516,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55455,7 +55606,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55625,7 +55776,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55768,7 +55919,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55865,7 +56016,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55960,7 +56111,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55982,7 +56133,7 @@ function zip() { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55998,7 +56149,7 @@ function zipAll(project) { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56007,8 +56158,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(161); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(142); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(144); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(490); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(491); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(491); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(492); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -56090,7 +56241,7 @@ function toArray(value) { } /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56243,7 +56394,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56251,12 +56402,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(492); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(493); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(496); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(497); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(271); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(272); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -56397,15 +56548,15 @@ class Kibana { } /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(148); -const arrayUnion = __webpack_require__(493); -const arrayDiffer = __webpack_require__(494); -const arrify = __webpack_require__(495); +const arrayUnion = __webpack_require__(494); +const arrayDiffer = __webpack_require__(495); +const arrify = __webpack_require__(496); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -56429,7 +56580,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56441,7 +56592,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56456,7 +56607,7 @@ module.exports = arrayDiffer; /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56486,7 +56637,7 @@ module.exports = arrify; /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56514,15 +56665,15 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(498); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(734); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(735); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -56547,19 +56698,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(499); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(500); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(280); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(271); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(272); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(129); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(142); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(163); @@ -56695,7 +56846,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56703,13 +56854,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(154); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pAll = __webpack_require__(500); -const arrify = __webpack_require__(502); -const globby = __webpack_require__(503); -const isGlob = __webpack_require__(718); -const cpFile = __webpack_require__(719); -const junk = __webpack_require__(731); -const CpyError = __webpack_require__(732); +const pAll = __webpack_require__(501); +const arrify = __webpack_require__(503); +const globby = __webpack_require__(504); +const isGlob = __webpack_require__(719); +const cpFile = __webpack_require__(720); +const junk = __webpack_require__(732); +const CpyError = __webpack_require__(733); const defaultOptions = { ignoreJunk: true @@ -56828,12 +56979,12 @@ module.exports = (source, destination, { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(501); +const pMap = __webpack_require__(502); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -56841,7 +56992,7 @@ module.exports.default = module.exports; /***/ }), -/* 501 */ +/* 502 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56920,7 +57071,7 @@ module.exports.default = pMap; /***/ }), -/* 502 */ +/* 503 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56950,17 +57101,17 @@ module.exports = arrify; /***/ }), -/* 503 */ +/* 504 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const arrayUnion = __webpack_require__(504); -const glob = __webpack_require__(506); -const fastGlob = __webpack_require__(511); -const dirGlob = __webpack_require__(711); -const gitignore = __webpack_require__(714); +const arrayUnion = __webpack_require__(505); +const glob = __webpack_require__(507); +const fastGlob = __webpack_require__(512); +const dirGlob = __webpack_require__(712); +const gitignore = __webpack_require__(715); const DEFAULT_FILTER = () => false; @@ -57105,12 +57256,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 504 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(505); +var arrayUniq = __webpack_require__(506); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -57118,7 +57269,7 @@ module.exports = function () { /***/ }), -/* 505 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -57187,7 +57338,7 @@ if ('Set' in global) { /***/ }), -/* 506 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -57236,13 +57387,13 @@ var fs = __webpack_require__(132) var rp = __webpack_require__(146) var minimatch = __webpack_require__(148) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(507) +var inherits = __webpack_require__(508) var EE = __webpack_require__(154).EventEmitter var path = __webpack_require__(4) var assert = __webpack_require__(138) var isAbsolute = __webpack_require__(155) -var globSync = __webpack_require__(509) -var common = __webpack_require__(510) +var globSync = __webpack_require__(510) +var common = __webpack_require__(511) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -57983,7 +58134,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 507 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -57993,12 +58144,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(508); + module.exports = __webpack_require__(509); } /***/ }), -/* 508 */ +/* 509 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -58031,7 +58182,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 509 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -58041,12 +58192,12 @@ var fs = __webpack_require__(132) var rp = __webpack_require__(146) var minimatch = __webpack_require__(148) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(506).Glob +var Glob = __webpack_require__(507).Glob var util = __webpack_require__(111) var path = __webpack_require__(4) var assert = __webpack_require__(138) var isAbsolute = __webpack_require__(155) -var common = __webpack_require__(510) +var common = __webpack_require__(511) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -58523,7 +58674,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 510 */ +/* 511 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -58769,10 +58920,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 511 */ +/* 512 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(512); +const pkg = __webpack_require__(513); module.exports = pkg.async; module.exports.default = pkg.async; @@ -58785,19 +58936,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 512 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(513); -var taskManager = __webpack_require__(514); -var reader_async_1 = __webpack_require__(682); -var reader_stream_1 = __webpack_require__(706); -var reader_sync_1 = __webpack_require__(707); -var arrayUtils = __webpack_require__(709); -var streamUtils = __webpack_require__(710); +var optionsManager = __webpack_require__(514); +var taskManager = __webpack_require__(515); +var reader_async_1 = __webpack_require__(683); +var reader_stream_1 = __webpack_require__(707); +var reader_sync_1 = __webpack_require__(708); +var arrayUtils = __webpack_require__(710); +var streamUtils = __webpack_require__(711); /** * Synchronous API. */ @@ -58863,7 +59014,7 @@ function isString(source) { /***/ }), -/* 513 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -58901,13 +59052,13 @@ exports.prepare = prepare; /***/ }), -/* 514 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(515); +var patternUtils = __webpack_require__(516); /** * Generate tasks based on parent directory of each pattern. */ @@ -58998,16 +59149,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 515 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(516); -var isGlob = __webpack_require__(519); -var micromatch = __webpack_require__(520); +var globParent = __webpack_require__(517); +var isGlob = __webpack_require__(520); +var micromatch = __webpack_require__(521); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -59153,15 +59304,15 @@ exports.matchAny = matchAny; /***/ }), -/* 516 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(517); -var pathDirname = __webpack_require__(518); +var isglob = __webpack_require__(518); +var pathDirname = __webpack_require__(519); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -59184,7 +59335,7 @@ module.exports = function globParent(str) { /***/ }), -/* 517 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -59194,7 +59345,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(299); +var isExtglob = __webpack_require__(300); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -59215,7 +59366,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 518 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59365,7 +59516,7 @@ module.exports.win32 = win32; /***/ }), -/* 519 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -59375,7 +59526,7 @@ module.exports.win32 = win32; * Released under the MIT License. */ -var isExtglob = __webpack_require__(299); +var isExtglob = __webpack_require__(300); var chars = { '{': '}', '(': ')', '[': ']'}; module.exports = function isGlob(str, options) { @@ -59417,7 +59568,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 520 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59428,18 +59579,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(111); -var braces = __webpack_require__(521); -var toRegex = __webpack_require__(634); -var extend = __webpack_require__(642); +var braces = __webpack_require__(522); +var toRegex = __webpack_require__(635); +var extend = __webpack_require__(643); /** * Local dependencies */ -var compilers = __webpack_require__(645); -var parsers = __webpack_require__(678); -var cache = __webpack_require__(679); -var utils = __webpack_require__(680); +var compilers = __webpack_require__(646); +var parsers = __webpack_require__(679); +var cache = __webpack_require__(680); +var utils = __webpack_require__(681); var MAX_LENGTH = 1024 * 64; /** @@ -60301,7 +60452,7 @@ module.exports = micromatch; /***/ }), -/* 521 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60311,18 +60462,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(522); -var unique = __webpack_require__(536); -var extend = __webpack_require__(531); +var toRegex = __webpack_require__(523); +var unique = __webpack_require__(537); +var extend = __webpack_require__(532); /** * Local dependencies */ -var compilers = __webpack_require__(537); -var parsers = __webpack_require__(554); -var Braces = __webpack_require__(564); -var utils = __webpack_require__(538); +var compilers = __webpack_require__(538); +var parsers = __webpack_require__(555); +var Braces = __webpack_require__(565); +var utils = __webpack_require__(539); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -60626,15 +60777,15 @@ module.exports = braces; /***/ }), -/* 522 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(523); -var extend = __webpack_require__(531); -var not = __webpack_require__(533); +var define = __webpack_require__(524); +var extend = __webpack_require__(532); +var not = __webpack_require__(534); var MAX_LENGTH = 1024 * 64; /** @@ -60781,7 +60932,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 523 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60794,7 +60945,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(524); +var isDescriptor = __webpack_require__(525); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -60819,7 +60970,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 524 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60832,9 +60983,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(525); -var isAccessor = __webpack_require__(526); -var isData = __webpack_require__(529); +var typeOf = __webpack_require__(526); +var isAccessor = __webpack_require__(527); +var isData = __webpack_require__(530); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -60848,7 +60999,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 525 */ +/* 526 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -61001,7 +61152,7 @@ function isBuffer(val) { /***/ }), -/* 526 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61014,7 +61165,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(527); +var typeOf = __webpack_require__(528); // accessor descriptor properties var accessor = { @@ -61077,10 +61228,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 527 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -61199,7 +61350,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 528 */ +/* 529 */ /***/ (function(module, exports) { /*! @@ -61226,7 +61377,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 529 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61239,7 +61390,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(530); +var typeOf = __webpack_require__(531); // data descriptor properties var data = { @@ -61288,10 +61439,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 530 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -61410,13 +61561,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 531 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(532); +var isObject = __webpack_require__(533); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -61450,7 +61601,7 @@ function hasOwn(obj, key) { /***/ }), -/* 532 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61470,13 +61621,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 533 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(534); +var extend = __webpack_require__(535); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -61543,13 +61694,13 @@ module.exports = toRegex; /***/ }), -/* 534 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(535); +var isObject = __webpack_require__(536); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -61583,7 +61734,7 @@ function hasOwn(obj, key) { /***/ }), -/* 535 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61603,7 +61754,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 536 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61653,13 +61804,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 537 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(538); +var utils = __webpack_require__(539); module.exports = function(braces, options) { braces.compiler @@ -61942,25 +62093,25 @@ function hasQueue(node) { /***/ }), -/* 538 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(539); +var splitString = __webpack_require__(540); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(531); -utils.flatten = __webpack_require__(545); -utils.isObject = __webpack_require__(543); -utils.fillRange = __webpack_require__(546); -utils.repeat = __webpack_require__(553); -utils.unique = __webpack_require__(536); +utils.extend = __webpack_require__(532); +utils.flatten = __webpack_require__(546); +utils.isObject = __webpack_require__(544); +utils.fillRange = __webpack_require__(547); +utils.repeat = __webpack_require__(554); +utils.unique = __webpack_require__(537); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -62292,7 +62443,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 539 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62305,7 +62456,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(540); +var extend = __webpack_require__(541); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -62470,14 +62621,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 540 */ +/* 541 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(541); -var assignSymbols = __webpack_require__(544); +var isExtendable = __webpack_require__(542); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -62537,7 +62688,7 @@ function isEnum(obj, key) { /***/ }), -/* 541 */ +/* 542 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62550,7 +62701,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(542); +var isPlainObject = __webpack_require__(543); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -62558,7 +62709,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 542 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62571,7 +62722,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(543); +var isObject = __webpack_require__(544); function isObjectObject(o) { return isObject(o) === true @@ -62602,7 +62753,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 543 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62621,7 +62772,7 @@ module.exports = function isObject(val) { /***/ }), -/* 544 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62668,7 +62819,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 545 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62697,7 +62848,7 @@ function flat(arr, res) { /***/ }), -/* 546 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62711,10 +62862,10 @@ function flat(arr, res) { var util = __webpack_require__(111); -var isNumber = __webpack_require__(547); -var extend = __webpack_require__(549); -var repeat = __webpack_require__(551); -var toRegex = __webpack_require__(552); +var isNumber = __webpack_require__(548); +var extend = __webpack_require__(550); +var repeat = __webpack_require__(552); +var toRegex = __webpack_require__(553); /** * Return a range of numbers or letters. @@ -62912,7 +63063,7 @@ module.exports = fillRange; /***/ }), -/* 547 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62925,7 +63076,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(548); +var typeOf = __webpack_require__(549); module.exports = function isNumber(num) { var type = typeOf(num); @@ -62941,10 +63092,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 548 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -63063,13 +63214,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 549 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(550); +var isObject = __webpack_require__(551); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63103,7 +63254,7 @@ function hasOwn(obj, key) { /***/ }), -/* 550 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63123,7 +63274,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 551 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63200,7 +63351,7 @@ function repeat(str, num) { /***/ }), -/* 552 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63213,8 +63364,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(551); -var isNumber = __webpack_require__(547); +var repeat = __webpack_require__(552); +var isNumber = __webpack_require__(548); var cache = {}; function toRegexRange(min, max, options) { @@ -63501,7 +63652,7 @@ module.exports = toRegexRange; /***/ }), -/* 553 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63526,14 +63677,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 554 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(555); -var utils = __webpack_require__(538); +var Node = __webpack_require__(556); +var utils = __webpack_require__(539); /** * Braces parsers @@ -63893,15 +64044,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 555 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(543); -var define = __webpack_require__(556); -var utils = __webpack_require__(563); +var isObject = __webpack_require__(544); +var define = __webpack_require__(557); +var utils = __webpack_require__(564); var ownNames; /** @@ -64392,7 +64543,7 @@ exports = module.exports = Node; /***/ }), -/* 556 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64405,7 +64556,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(557); +var isDescriptor = __webpack_require__(558); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -64430,7 +64581,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 557 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64443,9 +64594,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(558); -var isAccessor = __webpack_require__(559); -var isData = __webpack_require__(561); +var typeOf = __webpack_require__(559); +var isAccessor = __webpack_require__(560); +var isData = __webpack_require__(562); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -64459,7 +64610,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 558 */ +/* 559 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -64594,7 +64745,7 @@ function isBuffer(val) { /***/ }), -/* 559 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64607,7 +64758,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(560); +var typeOf = __webpack_require__(561); // accessor descriptor properties var accessor = { @@ -64670,7 +64821,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 560 */ +/* 561 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -64805,7 +64956,7 @@ function isBuffer(val) { /***/ }), -/* 561 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64818,7 +64969,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(562); +var typeOf = __webpack_require__(563); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -64861,7 +65012,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 562 */ +/* 563 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -64996,13 +65147,13 @@ function isBuffer(val) { /***/ }), -/* 563 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(548); +var typeOf = __webpack_require__(549); var utils = module.exports; /** @@ -66022,17 +66173,17 @@ function assert(val, message) { /***/ }), -/* 564 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(531); -var Snapdragon = __webpack_require__(565); -var compilers = __webpack_require__(537); -var parsers = __webpack_require__(554); -var utils = __webpack_require__(538); +var extend = __webpack_require__(532); +var Snapdragon = __webpack_require__(566); +var compilers = __webpack_require__(538); +var parsers = __webpack_require__(555); +var utils = __webpack_require__(539); /** * Customize Snapdragon parser and renderer @@ -66133,17 +66284,17 @@ module.exports = Braces; /***/ }), -/* 565 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(566); -var define = __webpack_require__(592); -var Compiler = __webpack_require__(602); -var Parser = __webpack_require__(631); -var utils = __webpack_require__(611); +var Base = __webpack_require__(567); +var define = __webpack_require__(593); +var Compiler = __webpack_require__(603); +var Parser = __webpack_require__(632); +var utils = __webpack_require__(612); var regexCache = {}; var cache = {}; @@ -66314,20 +66465,20 @@ module.exports.Parser = Parser; /***/ }), -/* 566 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(111); -var define = __webpack_require__(567); -var CacheBase = __webpack_require__(568); -var Emitter = __webpack_require__(569); -var isObject = __webpack_require__(543); -var merge = __webpack_require__(586); -var pascal = __webpack_require__(589); -var cu = __webpack_require__(590); +var define = __webpack_require__(568); +var CacheBase = __webpack_require__(569); +var Emitter = __webpack_require__(570); +var isObject = __webpack_require__(544); +var merge = __webpack_require__(587); +var pascal = __webpack_require__(590); +var cu = __webpack_require__(591); /** * Optionally define a custom `cache` namespace to use. @@ -66756,7 +66907,7 @@ module.exports.namespace = namespace; /***/ }), -/* 567 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66769,7 +66920,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(557); +var isDescriptor = __webpack_require__(558); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66794,21 +66945,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 568 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(543); -var Emitter = __webpack_require__(569); -var visit = __webpack_require__(570); -var toPath = __webpack_require__(573); -var union = __webpack_require__(574); -var del = __webpack_require__(578); -var get = __webpack_require__(576); -var has = __webpack_require__(583); -var set = __webpack_require__(577); +var isObject = __webpack_require__(544); +var Emitter = __webpack_require__(570); +var visit = __webpack_require__(571); +var toPath = __webpack_require__(574); +var union = __webpack_require__(575); +var del = __webpack_require__(579); +var get = __webpack_require__(577); +var has = __webpack_require__(584); +var set = __webpack_require__(578); /** * Create a `Cache` constructor that when instantiated will @@ -67062,7 +67213,7 @@ module.exports.namespace = namespace; /***/ }), -/* 569 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { @@ -67231,7 +67382,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 570 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67244,8 +67395,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(571); -var mapVisit = __webpack_require__(572); +var visit = __webpack_require__(572); +var mapVisit = __webpack_require__(573); module.exports = function(collection, method, val) { var result; @@ -67268,7 +67419,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 571 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67281,7 +67432,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(543); +var isObject = __webpack_require__(544); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -67308,14 +67459,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 572 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(111); -var visit = __webpack_require__(571); +var visit = __webpack_require__(572); /** * Map `visit` over an array of objects. @@ -67352,7 +67503,7 @@ function isObject(val) { /***/ }), -/* 573 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67365,7 +67516,7 @@ function isObject(val) { -var typeOf = __webpack_require__(548); +var typeOf = __webpack_require__(549); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -67392,16 +67543,16 @@ function filter(arr) { /***/ }), -/* 574 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(535); -var union = __webpack_require__(575); -var get = __webpack_require__(576); -var set = __webpack_require__(577); +var isObject = __webpack_require__(536); +var union = __webpack_require__(576); +var get = __webpack_require__(577); +var set = __webpack_require__(578); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -67429,7 +67580,7 @@ function arrayify(val) { /***/ }), -/* 575 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67465,7 +67616,7 @@ module.exports = function union(init) { /***/ }), -/* 576 */ +/* 577 */ /***/ (function(module, exports) { /*! @@ -67521,7 +67672,7 @@ function toString(val) { /***/ }), -/* 577 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67534,10 +67685,10 @@ function toString(val) { -var split = __webpack_require__(539); -var extend = __webpack_require__(534); -var isPlainObject = __webpack_require__(542); -var isObject = __webpack_require__(535); +var split = __webpack_require__(540); +var extend = __webpack_require__(535); +var isPlainObject = __webpack_require__(543); +var isObject = __webpack_require__(536); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -67583,7 +67734,7 @@ function isValidKey(key) { /***/ }), -/* 578 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67596,8 +67747,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(543); -var has = __webpack_require__(579); +var isObject = __webpack_require__(544); +var has = __webpack_require__(580); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -67622,7 +67773,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 579 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67635,9 +67786,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(580); -var hasValues = __webpack_require__(582); -var get = __webpack_require__(576); +var isObject = __webpack_require__(581); +var hasValues = __webpack_require__(583); +var get = __webpack_require__(577); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -67648,7 +67799,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 580 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67661,7 +67812,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(581); +var isArray = __webpack_require__(582); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -67669,7 +67820,7 @@ module.exports = function isObject(val) { /***/ }), -/* 581 */ +/* 582 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -67680,7 +67831,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 582 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67723,7 +67874,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 583 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67736,9 +67887,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(543); -var hasValues = __webpack_require__(584); -var get = __webpack_require__(576); +var isObject = __webpack_require__(544); +var hasValues = __webpack_require__(585); +var get = __webpack_require__(577); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -67746,7 +67897,7 @@ module.exports = function(val, prop) { /***/ }), -/* 584 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67759,8 +67910,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(585); -var isNumber = __webpack_require__(547); +var typeOf = __webpack_require__(586); +var isNumber = __webpack_require__(548); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -67813,10 +67964,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 585 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -67938,14 +68089,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 586 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(587); -var forIn = __webpack_require__(588); +var isExtendable = __webpack_require__(588); +var forIn = __webpack_require__(589); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -68009,7 +68160,7 @@ module.exports = mixinDeep; /***/ }), -/* 587 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68022,7 +68173,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(542); +var isPlainObject = __webpack_require__(543); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -68030,7 +68181,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 588 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68053,7 +68204,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 589 */ +/* 590 */ /***/ (function(module, exports) { /*! @@ -68080,14 +68231,14 @@ module.exports = pascalcase; /***/ }), -/* 590 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(111); -var utils = __webpack_require__(591); +var utils = __webpack_require__(592); /** * Expose class utils @@ -68452,7 +68603,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 591 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68466,10 +68617,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(575); -utils.define = __webpack_require__(592); -utils.isObj = __webpack_require__(543); -utils.staticExtend = __webpack_require__(599); +utils.union = __webpack_require__(576); +utils.define = __webpack_require__(593); +utils.isObj = __webpack_require__(544); +utils.staticExtend = __webpack_require__(600); /** @@ -68480,7 +68631,7 @@ module.exports = utils; /***/ }), -/* 592 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68493,7 +68644,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(593); +var isDescriptor = __webpack_require__(594); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68518,7 +68669,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 593 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68531,9 +68682,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(594); -var isAccessor = __webpack_require__(595); -var isData = __webpack_require__(597); +var typeOf = __webpack_require__(595); +var isAccessor = __webpack_require__(596); +var isData = __webpack_require__(598); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -68547,7 +68698,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 594 */ +/* 595 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -68700,7 +68851,7 @@ function isBuffer(val) { /***/ }), -/* 595 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68713,7 +68864,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(596); +var typeOf = __webpack_require__(597); // accessor descriptor properties var accessor = { @@ -68776,10 +68927,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 596 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -68898,7 +69049,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 597 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68911,7 +69062,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(598); +var typeOf = __webpack_require__(599); // data descriptor properties var data = { @@ -68960,10 +69111,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 598 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(528); +var isBuffer = __webpack_require__(529); var toString = Object.prototype.toString; /** @@ -69082,7 +69233,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 599 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69095,8 +69246,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(600); -var define = __webpack_require__(592); +var copy = __webpack_require__(601); +var define = __webpack_require__(593); var util = __webpack_require__(111); /** @@ -69179,15 +69330,15 @@ module.exports = extend; /***/ }), -/* 600 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(548); -var copyDescriptor = __webpack_require__(601); -var define = __webpack_require__(592); +var typeOf = __webpack_require__(549); +var copyDescriptor = __webpack_require__(602); +var define = __webpack_require__(593); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -69360,7 +69511,7 @@ module.exports.has = has; /***/ }), -/* 601 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69448,16 +69599,16 @@ function isObject(val) { /***/ }), -/* 602 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(603); -var define = __webpack_require__(592); -var debug = __webpack_require__(605)('snapdragon:compiler'); -var utils = __webpack_require__(611); +var use = __webpack_require__(604); +var define = __webpack_require__(593); +var debug = __webpack_require__(606)('snapdragon:compiler'); +var utils = __webpack_require__(612); /** * Create a new `Compiler` with the given `options`. @@ -69611,7 +69762,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(630); + var sourcemaps = __webpack_require__(631); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -69632,7 +69783,7 @@ module.exports = Compiler; /***/ }), -/* 603 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69645,7 +69796,7 @@ module.exports = Compiler; -var utils = __webpack_require__(604); +var utils = __webpack_require__(605); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -69760,7 +69911,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 604 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69774,8 +69925,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(592); -utils.isObject = __webpack_require__(543); +utils.define = __webpack_require__(593); +utils.isObject = __webpack_require__(544); utils.isString = function(val) { @@ -69790,7 +69941,7 @@ module.exports = utils; /***/ }), -/* 605 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -69799,14 +69950,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(606); + module.exports = __webpack_require__(607); } else { - module.exports = __webpack_require__(609); + module.exports = __webpack_require__(610); } /***/ }), -/* 606 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -69815,7 +69966,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(607); +exports = module.exports = __webpack_require__(608); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -69997,7 +70148,7 @@ function localstorage() { /***/ }), -/* 607 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { @@ -70013,7 +70164,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(608); +exports.humanize = __webpack_require__(609); /** * The currently active debug mode names, and names to skip. @@ -70205,7 +70356,7 @@ function coerce(val) { /***/ }), -/* 608 */ +/* 609 */ /***/ (function(module, exports) { /** @@ -70363,14 +70514,14 @@ function plural(ms, n, name) { /***/ }), -/* 609 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(277); +var tty = __webpack_require__(278); var util = __webpack_require__(111); /** @@ -70379,7 +70530,7 @@ var util = __webpack_require__(111); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(607); +exports = module.exports = __webpack_require__(608); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -70558,7 +70709,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(610); + var net = __webpack_require__(611); stream = new net.Socket({ fd: fd, readable: false, @@ -70617,13 +70768,13 @@ exports.enable(load()); /***/ }), -/* 610 */ +/* 611 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 611 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70633,9 +70784,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(534); -exports.SourceMap = __webpack_require__(612); -exports.sourceMapResolve = __webpack_require__(623); +exports.extend = __webpack_require__(535); +exports.SourceMap = __webpack_require__(613); +exports.sourceMapResolve = __webpack_require__(624); /** * Convert backslash in the given string to forward slashes @@ -70678,7 +70829,7 @@ exports.last = function(arr, n) { /***/ }), -/* 612 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -70686,13 +70837,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(613).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(619).SourceMapConsumer; -exports.SourceNode = __webpack_require__(622).SourceNode; +exports.SourceMapGenerator = __webpack_require__(614).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(620).SourceMapConsumer; +exports.SourceNode = __webpack_require__(623).SourceNode; /***/ }), -/* 613 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -70702,10 +70853,10 @@ exports.SourceNode = __webpack_require__(622).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(614); -var util = __webpack_require__(616); -var ArraySet = __webpack_require__(617).ArraySet; -var MappingList = __webpack_require__(618).MappingList; +var base64VLQ = __webpack_require__(615); +var util = __webpack_require__(617); +var ArraySet = __webpack_require__(618).ArraySet; +var MappingList = __webpack_require__(619).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -71114,7 +71265,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 614 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71154,7 +71305,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(615); +var base64 = __webpack_require__(616); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -71260,7 +71411,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 615 */ +/* 616 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71333,7 +71484,7 @@ exports.decode = function (charCode) { /***/ }), -/* 616 */ +/* 617 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71756,7 +71907,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 617 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71766,7 +71917,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(616); +var util = __webpack_require__(617); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -71883,7 +72034,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 618 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71893,7 +72044,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(616); +var util = __webpack_require__(617); /** * Determine whether mappingB is after mappingA with respect to generated @@ -71968,7 +72119,7 @@ exports.MappingList = MappingList; /***/ }), -/* 619 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71978,11 +72129,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(616); -var binarySearch = __webpack_require__(620); -var ArraySet = __webpack_require__(617).ArraySet; -var base64VLQ = __webpack_require__(614); -var quickSort = __webpack_require__(621).quickSort; +var util = __webpack_require__(617); +var binarySearch = __webpack_require__(621); +var ArraySet = __webpack_require__(618).ArraySet; +var base64VLQ = __webpack_require__(615); +var quickSort = __webpack_require__(622).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -73056,7 +73207,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 620 */ +/* 621 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73173,7 +73324,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 621 */ +/* 622 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73293,7 +73444,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 622 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73303,8 +73454,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(613).SourceMapGenerator; -var util = __webpack_require__(616); +var SourceMapGenerator = __webpack_require__(614).SourceMapGenerator; +var util = __webpack_require__(617); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -73712,17 +73863,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 623 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(624) -var resolveUrl = __webpack_require__(625) -var decodeUriComponent = __webpack_require__(626) -var urix = __webpack_require__(628) -var atob = __webpack_require__(629) +var sourceMappingURL = __webpack_require__(625) +var resolveUrl = __webpack_require__(626) +var decodeUriComponent = __webpack_require__(627) +var urix = __webpack_require__(629) +var atob = __webpack_require__(630) @@ -74020,7 +74171,7 @@ module.exports = { /***/ }), -/* 624 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -74083,7 +74234,7 @@ void (function(root, factory) { /***/ }), -/* 625 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -74101,13 +74252,13 @@ module.exports = resolveUrl /***/ }), -/* 626 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(627) +var decodeUriComponent = __webpack_require__(628) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -74118,7 +74269,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 627 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74219,7 +74370,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 628 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -74242,7 +74393,7 @@ module.exports = urix /***/ }), -/* 629 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74256,7 +74407,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 630 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74264,8 +74415,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(132); var path = __webpack_require__(4); -var define = __webpack_require__(592); -var utils = __webpack_require__(611); +var define = __webpack_require__(593); +var utils = __webpack_require__(612); /** * Expose `mixin()`. @@ -74408,19 +74559,19 @@ exports.comment = function(node) { /***/ }), -/* 631 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(603); +var use = __webpack_require__(604); var util = __webpack_require__(111); -var Cache = __webpack_require__(632); -var define = __webpack_require__(592); -var debug = __webpack_require__(605)('snapdragon:parser'); -var Position = __webpack_require__(633); -var utils = __webpack_require__(611); +var Cache = __webpack_require__(633); +var define = __webpack_require__(593); +var debug = __webpack_require__(606)('snapdragon:parser'); +var Position = __webpack_require__(634); +var utils = __webpack_require__(612); /** * Create a new `Parser` with the given `input` and `options`. @@ -74948,7 +75099,7 @@ module.exports = Parser; /***/ }), -/* 632 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75055,13 +75206,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 633 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(592); +var define = __webpack_require__(593); /** * Store position for a node @@ -75076,16 +75227,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 634 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(635); -var define = __webpack_require__(641); -var extend = __webpack_require__(642); -var not = __webpack_require__(644); +var safe = __webpack_require__(636); +var define = __webpack_require__(642); +var extend = __webpack_require__(643); +var not = __webpack_require__(645); var MAX_LENGTH = 1024 * 64; /** @@ -75238,10 +75389,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 635 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(636); +var parse = __webpack_require__(637); var types = parse.types; module.exports = function (re, opts) { @@ -75287,13 +75438,13 @@ function isRegExp (x) { /***/ }), -/* 636 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(637); -var types = __webpack_require__(638); -var sets = __webpack_require__(639); -var positions = __webpack_require__(640); +var util = __webpack_require__(638); +var types = __webpack_require__(639); +var sets = __webpack_require__(640); +var positions = __webpack_require__(641); module.exports = function(regexpStr) { @@ -75575,11 +75726,11 @@ module.exports.types = types; /***/ }), -/* 637 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(638); -var sets = __webpack_require__(639); +var types = __webpack_require__(639); +var sets = __webpack_require__(640); // All of these are private and only used by randexp. @@ -75692,7 +75843,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 638 */ +/* 639 */ /***/ (function(module, exports) { module.exports = { @@ -75708,10 +75859,10 @@ module.exports = { /***/ }), -/* 639 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(638); +var types = __webpack_require__(639); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -75796,10 +75947,10 @@ exports.anyChar = function() { /***/ }), -/* 640 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(638); +var types = __webpack_require__(639); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -75819,7 +75970,7 @@ exports.end = function() { /***/ }), -/* 641 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75832,8 +75983,8 @@ exports.end = function() { -var isobject = __webpack_require__(543); -var isDescriptor = __webpack_require__(557); +var isobject = __webpack_require__(544); +var isDescriptor = __webpack_require__(558); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -75864,14 +76015,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 642 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(643); -var assignSymbols = __webpack_require__(544); +var isExtendable = __webpack_require__(644); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -75931,7 +76082,7 @@ function isEnum(obj, key) { /***/ }), -/* 643 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75944,7 +76095,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(542); +var isPlainObject = __webpack_require__(543); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -75952,14 +76103,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 644 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(642); -var safe = __webpack_require__(635); +var extend = __webpack_require__(643); +var safe = __webpack_require__(636); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -76031,14 +76182,14 @@ module.exports = toRegex; /***/ }), -/* 645 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(646); -var extglob = __webpack_require__(662); +var nanomatch = __webpack_require__(647); +var extglob = __webpack_require__(663); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76115,7 +76266,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 646 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76126,17 +76277,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(111); -var toRegex = __webpack_require__(647); -var extend = __webpack_require__(648); +var toRegex = __webpack_require__(648); +var extend = __webpack_require__(649); /** * Local dependencies */ -var compilers = __webpack_require__(650); -var parsers = __webpack_require__(651); -var cache = __webpack_require__(654); -var utils = __webpack_require__(656); +var compilers = __webpack_require__(651); +var parsers = __webpack_require__(652); +var cache = __webpack_require__(655); +var utils = __webpack_require__(657); var MAX_LENGTH = 1024 * 64; /** @@ -76960,15 +77111,15 @@ module.exports = nanomatch; /***/ }), -/* 647 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(592); -var extend = __webpack_require__(534); -var not = __webpack_require__(533); +var define = __webpack_require__(593); +var extend = __webpack_require__(535); +var not = __webpack_require__(534); var MAX_LENGTH = 1024 * 64; /** @@ -77115,14 +77266,14 @@ module.exports.makeRe = makeRe; /***/ }), -/* 648 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(649); -var assignSymbols = __webpack_require__(544); +var isExtendable = __webpack_require__(650); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77182,7 +77333,7 @@ function isEnum(obj, key) { /***/ }), -/* 649 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77195,7 +77346,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(542); +var isPlainObject = __webpack_require__(543); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77203,7 +77354,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 650 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77549,15 +77700,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 651 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(533); -var toRegex = __webpack_require__(647); -var isOdd = __webpack_require__(652); +var regexNot = __webpack_require__(534); +var toRegex = __webpack_require__(648); +var isOdd = __webpack_require__(653); /** * Characters to use in negation regex (we want to "not" match @@ -77943,7 +78094,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 652 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77956,7 +78107,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(653); +var isNumber = __webpack_require__(654); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -77970,7 +78121,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 653 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77998,14 +78149,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 654 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(655))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 655 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78018,7 +78169,7 @@ module.exports = new (__webpack_require__(655))(); -var MapCache = __webpack_require__(632); +var MapCache = __webpack_require__(633); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78140,7 +78291,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 656 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78153,14 +78304,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(657)(); -var Snapdragon = __webpack_require__(565); -utils.define = __webpack_require__(658); -utils.diff = __webpack_require__(659); -utils.extend = __webpack_require__(648); -utils.pick = __webpack_require__(660); -utils.typeOf = __webpack_require__(661); -utils.unique = __webpack_require__(536); +var isWindows = __webpack_require__(658)(); +var Snapdragon = __webpack_require__(566); +utils.define = __webpack_require__(659); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(649); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(662); +utils.unique = __webpack_require__(537); /** * Returns true if the given value is effectively an empty string @@ -78526,7 +78677,7 @@ utils.unixify = function(options) { /***/ }), -/* 657 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -78554,7 +78705,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 658 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78567,8 +78718,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(543); -var isDescriptor = __webpack_require__(557); +var isobject = __webpack_require__(544); +var isDescriptor = __webpack_require__(558); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -78599,7 +78750,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 659 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78653,7 +78804,7 @@ function diffArray(one, two) { /***/ }), -/* 660 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78666,7 +78817,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(543); +var isObject = __webpack_require__(544); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -78695,7 +78846,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 661 */ +/* 662 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -78830,7 +78981,7 @@ function isBuffer(val) { /***/ }), -/* 662 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78840,18 +78991,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(534); -var unique = __webpack_require__(536); -var toRegex = __webpack_require__(647); +var extend = __webpack_require__(535); +var unique = __webpack_require__(537); +var toRegex = __webpack_require__(648); /** * Local dependencies */ -var compilers = __webpack_require__(663); -var parsers = __webpack_require__(674); -var Extglob = __webpack_require__(677); -var utils = __webpack_require__(676); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(675); +var Extglob = __webpack_require__(678); +var utils = __webpack_require__(677); var MAX_LENGTH = 1024 * 64; /** @@ -79168,13 +79319,13 @@ module.exports = extglob; /***/ }), -/* 663 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(664); +var brackets = __webpack_require__(665); /** * Extglob compilers @@ -79344,7 +79495,7 @@ module.exports = function(extglob) { /***/ }), -/* 664 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79354,17 +79505,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(665); -var parsers = __webpack_require__(667); +var compilers = __webpack_require__(666); +var parsers = __webpack_require__(668); /** * Module dependencies */ -var debug = __webpack_require__(669)('expand-brackets'); -var extend = __webpack_require__(534); -var Snapdragon = __webpack_require__(565); -var toRegex = __webpack_require__(647); +var debug = __webpack_require__(670)('expand-brackets'); +var extend = __webpack_require__(535); +var Snapdragon = __webpack_require__(566); +var toRegex = __webpack_require__(648); /** * Parses the given POSIX character class `pattern` and returns a @@ -79562,13 +79713,13 @@ module.exports = brackets; /***/ }), -/* 665 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(666); +var posix = __webpack_require__(667); module.exports = function(brackets) { brackets.compiler @@ -79656,7 +79807,7 @@ module.exports = function(brackets) { /***/ }), -/* 666 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79685,14 +79836,14 @@ module.exports = { /***/ }), -/* 667 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(668); -var define = __webpack_require__(592); +var utils = __webpack_require__(669); +var define = __webpack_require__(593); /** * Text regex @@ -79911,14 +80062,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 668 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(647); -var regexNot = __webpack_require__(533); +var toRegex = __webpack_require__(648); +var regexNot = __webpack_require__(534); var cached; /** @@ -79952,7 +80103,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 669 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -79961,14 +80112,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(670); + module.exports = __webpack_require__(671); } else { - module.exports = __webpack_require__(673); + module.exports = __webpack_require__(674); } /***/ }), -/* 670 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -79977,7 +80128,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(671); +exports = module.exports = __webpack_require__(672); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -80159,7 +80310,7 @@ function localstorage() { /***/ }), -/* 671 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { @@ -80175,7 +80326,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(672); +exports.humanize = __webpack_require__(673); /** * The currently active debug mode names, and names to skip. @@ -80367,7 +80518,7 @@ function coerce(val) { /***/ }), -/* 672 */ +/* 673 */ /***/ (function(module, exports) { /** @@ -80525,14 +80676,14 @@ function plural(ms, n, name) { /***/ }), -/* 673 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(277); +var tty = __webpack_require__(278); var util = __webpack_require__(111); /** @@ -80541,7 +80692,7 @@ var util = __webpack_require__(111); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(671); +exports = module.exports = __webpack_require__(672); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -80720,7 +80871,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(610); + var net = __webpack_require__(611); stream = new net.Socket({ fd: fd, readable: false, @@ -80779,15 +80930,15 @@ exports.enable(load()); /***/ }), -/* 674 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(664); -var define = __webpack_require__(675); -var utils = __webpack_require__(676); +var brackets = __webpack_require__(665); +var define = __webpack_require__(676); +var utils = __webpack_require__(677); /** * Characters to use in text regex (we want to "not" match @@ -80942,7 +81093,7 @@ module.exports = parsers; /***/ }), -/* 675 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80955,7 +81106,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(557); +var isDescriptor = __webpack_require__(558); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80980,14 +81131,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 676 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(533); -var Cache = __webpack_require__(655); +var regex = __webpack_require__(534); +var Cache = __webpack_require__(656); /** * Utils @@ -81056,7 +81207,7 @@ utils.createRegex = function(str) { /***/ }), -/* 677 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81066,16 +81217,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(565); -var define = __webpack_require__(675); -var extend = __webpack_require__(534); +var Snapdragon = __webpack_require__(566); +var define = __webpack_require__(676); +var extend = __webpack_require__(535); /** * Local dependencies */ -var compilers = __webpack_require__(663); -var parsers = __webpack_require__(674); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(675); /** * Customize Snapdragon parser and renderer @@ -81141,16 +81292,16 @@ module.exports = Extglob; /***/ }), -/* 678 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(662); -var nanomatch = __webpack_require__(646); -var regexNot = __webpack_require__(533); -var toRegex = __webpack_require__(634); +var extglob = __webpack_require__(663); +var nanomatch = __webpack_require__(647); +var regexNot = __webpack_require__(534); +var toRegex = __webpack_require__(635); var not; /** @@ -81231,14 +81382,14 @@ function textRegex(pattern) { /***/ }), -/* 679 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(655))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 680 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81251,13 +81402,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(565); -utils.define = __webpack_require__(641); -utils.diff = __webpack_require__(659); -utils.extend = __webpack_require__(642); -utils.pick = __webpack_require__(660); -utils.typeOf = __webpack_require__(681); -utils.unique = __webpack_require__(536); +var Snapdragon = __webpack_require__(566); +utils.define = __webpack_require__(642); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(643); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(682); +utils.unique = __webpack_require__(537); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81554,7 +81705,7 @@ utils.unixify = function(options) { /***/ }), -/* 681 */ +/* 682 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81689,7 +81840,7 @@ function isBuffer(val) { /***/ }), -/* 682 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81708,9 +81859,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(683); -var reader_1 = __webpack_require__(696); -var fs_stream_1 = __webpack_require__(700); +var readdir = __webpack_require__(684); +var reader_1 = __webpack_require__(697); +var fs_stream_1 = __webpack_require__(701); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81771,15 +81922,15 @@ exports.default = ReaderAsync; /***/ }), -/* 683 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(684); -const readdirAsync = __webpack_require__(692); -const readdirStream = __webpack_require__(695); +const readdirSync = __webpack_require__(685); +const readdirAsync = __webpack_require__(693); +const readdirStream = __webpack_require__(696); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81863,7 +82014,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 684 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81871,11 +82022,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(685); +const DirectoryReader = __webpack_require__(686); let syncFacade = { - fs: __webpack_require__(690), - forEach: __webpack_require__(691), + fs: __webpack_require__(691), + forEach: __webpack_require__(692), sync: true }; @@ -81904,7 +82055,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 685 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81913,9 +82064,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(136).Readable; const EventEmitter = __webpack_require__(154).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(686); -const stat = __webpack_require__(688); -const call = __webpack_require__(689); +const normalizeOptions = __webpack_require__(687); +const stat = __webpack_require__(689); +const call = __webpack_require__(690); /** * Asynchronously reads the contents of a directory and streams the results @@ -82291,14 +82442,14 @@ module.exports = DirectoryReader; /***/ }), -/* 686 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(687); +const globToRegExp = __webpack_require__(688); module.exports = normalizeOptions; @@ -82475,7 +82626,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 687 */ +/* 688 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82612,13 +82763,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 688 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(689); +const call = __webpack_require__(690); module.exports = stat; @@ -82693,7 +82844,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 689 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82754,14 +82905,14 @@ function callOnce (fn) { /***/ }), -/* 690 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const call = __webpack_require__(689); +const call = __webpack_require__(690); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82825,7 +82976,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 691 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82854,7 +83005,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 692 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82862,12 +83013,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(693); -const DirectoryReader = __webpack_require__(685); +const maybe = __webpack_require__(694); +const DirectoryReader = __webpack_require__(686); let asyncFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(694), + forEach: __webpack_require__(695), async: true }; @@ -82909,7 +83060,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 693 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82936,7 +83087,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 694 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82972,7 +83123,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 695 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82980,11 +83131,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(685); +const DirectoryReader = __webpack_require__(686); let streamFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(694), + forEach: __webpack_require__(695), async: true }; @@ -83004,16 +83155,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 696 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(697); -var entry_1 = __webpack_require__(699); -var pathUtil = __webpack_require__(698); +var deep_1 = __webpack_require__(698); +var entry_1 = __webpack_require__(700); +var pathUtil = __webpack_require__(699); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -83079,14 +83230,14 @@ exports.default = Reader; /***/ }), -/* 697 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(698); -var patternUtils = __webpack_require__(515); +var pathUtils = __webpack_require__(699); +var patternUtils = __webpack_require__(516); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -83169,7 +83320,7 @@ exports.default = DeepFilter; /***/ }), -/* 698 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83200,14 +83351,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 699 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(698); -var patternUtils = __webpack_require__(515); +var pathUtils = __webpack_require__(699); +var patternUtils = __webpack_require__(516); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -83292,7 +83443,7 @@ exports.default = EntryFilter; /***/ }), -/* 700 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83312,8 +83463,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(136); -var fsStat = __webpack_require__(701); -var fs_1 = __webpack_require__(705); +var fsStat = __webpack_require__(702); +var fs_1 = __webpack_require__(706); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83363,14 +83514,14 @@ exports.default = FileSystemStream; /***/ }), -/* 701 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(702); -const statProvider = __webpack_require__(704); +const optionsManager = __webpack_require__(703); +const statProvider = __webpack_require__(705); /** * Asynchronous API. */ @@ -83401,13 +83552,13 @@ exports.statSync = statSync; /***/ }), -/* 702 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(703); +const fsAdapter = __webpack_require__(704); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83420,7 +83571,7 @@ exports.prepare = prepare; /***/ }), -/* 703 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83443,7 +83594,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 704 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83495,7 +83646,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 705 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83526,7 +83677,7 @@ exports.default = FileSystem; /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83546,9 +83697,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(136); -var readdir = __webpack_require__(683); -var reader_1 = __webpack_require__(696); -var fs_stream_1 = __webpack_require__(700); +var readdir = __webpack_require__(684); +var reader_1 = __webpack_require__(697); +var fs_stream_1 = __webpack_require__(701); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83616,7 +83767,7 @@ exports.default = ReaderStream; /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83635,9 +83786,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(683); -var reader_1 = __webpack_require__(696); -var fs_sync_1 = __webpack_require__(708); +var readdir = __webpack_require__(684); +var reader_1 = __webpack_require__(697); +var fs_sync_1 = __webpack_require__(709); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83697,7 +83848,7 @@ exports.default = ReaderSync; /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83716,8 +83867,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(701); -var fs_1 = __webpack_require__(705); +var fsStat = __webpack_require__(702); +var fs_1 = __webpack_require__(706); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83763,7 +83914,7 @@ exports.default = FileSystemSync; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83779,13 +83930,13 @@ exports.flatten = flatten; /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(283); +var merge2 = __webpack_require__(284); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ @@ -83800,13 +83951,13 @@ exports.merge = merge; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(712); +const pathType = __webpack_require__(713); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83872,13 +84023,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const pify = __webpack_require__(713); +const pify = __webpack_require__(714); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83921,7 +84072,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84012,17 +84163,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(511); -const gitIgnore = __webpack_require__(715); -const pify = __webpack_require__(716); -const slash = __webpack_require__(717); +const fastGlob = __webpack_require__(512); +const gitIgnore = __webpack_require__(716); +const pify = __webpack_require__(717); +const slash = __webpack_require__(718); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -84120,7 +84271,7 @@ module.exports.sync = options => { /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84589,7 +84740,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84664,7 +84815,7 @@ module.exports = (input, options) => { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84682,7 +84833,7 @@ module.exports = input => { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84692,7 +84843,7 @@ module.exports = input => { * Released under the MIT License. */ -var isExtglob = __webpack_require__(299); +var isExtglob = __webpack_require__(300); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -84736,17 +84887,17 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(132); -const pEvent = __webpack_require__(720); -const CpFileError = __webpack_require__(723); -const fs = __webpack_require__(727); -const ProgressEmitter = __webpack_require__(730); +const pEvent = __webpack_require__(721); +const CpFileError = __webpack_require__(724); +const fs = __webpack_require__(728); +const ProgressEmitter = __webpack_require__(731); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84860,12 +85011,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(721); +const pTimeout = __webpack_require__(722); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -85156,12 +85307,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(722); +const pFinally = __webpack_require__(723); class TimeoutError extends Error { constructor(message) { @@ -85207,7 +85358,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85229,12 +85380,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(724); +const NestedError = __webpack_require__(725); class CpFileError extends NestedError { constructor(message, nested) { @@ -85248,10 +85399,10 @@ module.exports = CpFileError; /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(725); +var inherits = __webpack_require__(726); var NestedError = function (message, nested) { this.nested = nested; @@ -85302,7 +85453,7 @@ module.exports = NestedError; /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -85310,12 +85461,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(726); + module.exports = __webpack_require__(727); } /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -85344,16 +85495,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(111); const fs = __webpack_require__(131); -const makeDir = __webpack_require__(728); -const pEvent = __webpack_require__(720); -const CpFileError = __webpack_require__(723); +const makeDir = __webpack_require__(729); +const pEvent = __webpack_require__(721); +const CpFileError = __webpack_require__(724); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85450,7 +85601,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85458,7 +85609,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(132); const path = __webpack_require__(4); const {promisify} = __webpack_require__(111); -const semver = __webpack_require__(729); +const semver = __webpack_require__(730); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85613,7 +85764,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -87215,7 +87366,7 @@ function coerce (version, options) { /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87256,7 +87407,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87302,12 +87453,12 @@ exports.default = module.exports; /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(733); +const NestedError = __webpack_require__(734); class CpyError extends NestedError { constructor(message, nested) { @@ -87321,7 +87472,7 @@ module.exports = CpyError; /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(111).inherits; @@ -87377,7 +87528,7 @@ module.exports = NestedError; /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson index 81267cd514f1f..db19c937ca990 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson @@ -1,7 +1,7 @@ { // Adapted from Vega's https://vega.github.io/vega/examples/stacked-area-chart/ - $schema: https://vega.github.io/schema/vega/v3.0.json + $schema: https://vega.github.io/schema/vega/v5.json data: [ { name: table diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson index cef73c0155794..633b8658ad849 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson @@ -1,7 +1,7 @@ # This graph creates a single rectangle for the whole graph on top of a map # Note that the actual map tiles are not loaded { - $schema: https://vega.github.io/schema/vega/v3.0.json + $schema: https://vega.github.io/schema/vega/v5.json config: { kibana: {type: "map", mapStyle: false} } diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson index 78e92a00384c6..77465c8b3f007 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson @@ -24,7 +24,7 @@ ] } ] - $schema: https://vega.github.io/schema/vega/v3.json + $schema: https://vega.github.io/schema/vega/v5.json marks: [ { from: {data: "table"} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 485390dc50a79..17610702a0bc7 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -333,7 +333,7 @@ describe('VegaVisualizations', () => { vegaVis = new VegaVisualization(domNode, vis); const vegaParser = new VegaParser( `{ - "$schema": "https://vega.github.io/schema/vega/v3.json", + "$schema": "https://vega.github.io/schema/vega/v5.json", "marks": [ { "type": "text", @@ -366,11 +366,6 @@ describe('VegaVisualizations', () => { await vegaVis.render(vegaParser, vis.params, { data: true }); const vegaView = vegaVis._vegaView._view; expect(vegaView.height()).to.be(250.00000001); - - vegaView.height(250); - await vegaView.runAsync(); - // as soon as this test fails, the workaround with the subpixel value can be removed. - expect(vegaView.height()).to.be(0); } finally { vegaVis.destroy(); } diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson index fd7eb1ae7d878..2132b0f77e6bc 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson @@ -1,5 +1,5 @@ { - $schema: https://vega.github.io/schema/vega-lite/v2.json + $schema: https://vega.github.io/schema/vega-lite/v4.json data: { format: {property: "aggregations.time_buckets.buckets"} values: { diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index d60bf7df899e3..f7ee9aa056762 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -24,7 +24,7 @@ export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, /node_modules[\/\\](mocha|moment)[\/\\]/, - /node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, + /node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/, ], webpackAliases: { diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 1cce11aea37fb..c1f7fcb75b149 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -420,7 +420,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Airport Connections (Hover Over Airport)', }), visState: - '{"aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v3.0.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\"OriginAirportID\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"OriginLocation\\", \\"Origin\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\"DestAirportID\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"DestLocation\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.origins.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\"!selected\\", remove: true}\\n {trigger: \\"selected\\", insert: \\"selected\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\"@airport:mouseover\\", update: \\"datum\\"}\\n {events: \\"@airport:mouseout\\", update: \\"null\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n {signal: \\"zoom*zoom*0.2+1\\"}\\n {signal: \\"zoom*zoom*10+1\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\"formula\\", expr: \\"{x:parent.x, y:parent.y}\\", as: \\"source\\"}\\n {type: \\"formula\\", expr: \\"{x:datum.x, y:datum.y}\\", as: \\"target\\"}\\n {type: \\"linkpath\\", shape: \\"diagonal\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: linear\\n domain: {data: \\"facetDatumElems\\", field: \\"doc_count\\"}\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: linear\\n domain: {data: \\"facetDatumElems\\", field: \\"doc_count\\"}\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\"facetDatumElems\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\"path\\"}\\n stroke: {value: \\"black\\"}\\n strokeWidth: {scale: \\"lineThickness\\", field: \\"doc_count\\"}\\n strokeOpacity: {scale: \\"lineOpacity\\", field: \\"doc_count\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"airportSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{title: datum.originLocation.hits.hits[0]._source.Origin + \' (\' + datum.key + \')\', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"},"title":"[Flights] Airport Connections (Hover Over Airport)","type":"vega"}', + '{"aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\"OriginAirportID\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"OriginLocation\\", \\"Origin\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\"DestAirportID\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"DestLocation\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.origins.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\"!selected\\", remove: true}\\n {trigger: \\"selected\\", insert: \\"selected\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\"@airport:mouseover\\", update: \\"datum\\"}\\n {events: \\"@airport:mouseout\\", update: \\"null\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n {signal: \\"zoom*zoom*0.2+1\\"}\\n {signal: \\"zoom*zoom*10+1\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\"formula\\", expr: \\"{x:parent.x, y:parent.y}\\", as: \\"source\\"}\\n {type: \\"formula\\", expr: \\"{x:datum.x, y:datum.y}\\", as: \\"target\\"}\\n {type: \\"linkpath\\", shape: \\"diagonal\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\"facetDatumElems\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\"path\\"}\\n stroke: {value: \\"black\\"}\\n strokeWidth: {scale: \\"lineThickness\\", field: \\"doc_count\\"}\\n strokeOpacity: {scale: \\"lineOpacity\\", field: \\"doc_count\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"airportSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{title: datum.originLocation.hits.hits[0]._source.Origin + \' (\' + datum.key + \')\', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"},"title":"[Flights] Airport Connections (Hover Over Airport)","type":"vega"}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 883108651bfc5..0620e93118b8d 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -145,7 +145,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Logs] File Type Scatter Plot', }), visState: - '{"title":"[Logs] File Type Scatter Plot","type":"vega","params":{"spec":"{\\n $schema: \\"https://vega.github.io/schema/vega-lite/v2.json\\"\\n // Use points for drawing to actually create a scatterplot\\n mark: point\\n // Specify where to load data from\\n data: {\\n // By using an object to the url parameter we will\\n // construct an Elasticsearch query\\n url: {\\n // Context == true means filters of the dashboard will be taken into account\\n %context%: true\\n // Specify on which field the time picker should operate\\n %timefield%: timestamp\\n // Specify the index pattern to load data from\\n index: kibana_sample_data_logs\\n // This body will be send to Elasticsearch\'s _search endpoint\\n // You can use everything the ES Query DSL supports here\\n body: {\\n // Set the size to load 10000 documents\\n size: 10000,\\n // Just ask for the fields we actually need for visualization\\n _source: [\\"timestamp\\", \\"bytes\\", \\"extension\\"]\\n }\\n }\\n // Tell Vega, that the array of data will be inside hits.hits of the response\\n // since the result returned from Elasticsearch fill have a format like:\\n // {\\n // hits: {\\n // total: 42000,\\n // max_score: 2,\\n // hits: [\\n // < our individual documents >\\n // ]\\n // }\\n // }\\n format: { property: \\"hits.hits\\" }\\n }\\n // You can do transformation and calculation of the data before drawing it\\n transform: [\\n // Since timestamp is a string value, we need to convert it to a unix timestamp\\n // so that Vega can work on it properly.\\n {\\n // Convert _source.timestamp field to a date\\n calculate: \\"toDate(datum._source[\'timestamp\'])\\"\\n // Store the result in a field named \\"time\\" in the object\\n as: \\"time\\"\\n }\\n ]\\n // Specify what data will be drawn on which axis\\n encoding: {\\n x: {\\n // Draw the time field on the x-axis in temporal mode (i.e. as a time axis)\\n field: time\\n type: temporal\\n // Hide the axis label for the x-axis\\n axis: { title: false }\\n }\\n y: {\\n // Draw the bytes of each document on the y-axis\\n field: _source.bytes\\n // Mark the y-axis as quantitative\\n type: quantitative\\n // Specify the label for this axis\\n axis: { title: \\"Transferred bytes\\" }\\n }\\n color: {\\n // Make the color of each point depend on the _source.extension field\\n field: _source.extension\\n // Treat different values as completely unrelated values to each other.\\n // You could switch this to quantitative if you have a numeric field and\\n // want to create a color scale from one color to another depending on that\\n // field\'s value.\\n type: nominal\\n // Rename the legend title so it won\'t just state: \\"_source.extension\\"\\n legend: { title: \'File type\' }\\n }\\n shape: {\\n // Also make the shape of each point dependent on the extension.\\n field: _source.extension\\n type: nominal\\n }\\n }\\n}"},"aggs":[]}', + '{"title":"[Logs] File Type Scatter Plot","type":"vega","params":{"spec":"{\\n $schema: \\"https://vega.github.io/schema/vega-lite/v4.json\\"\\n // Use points for drawing to actually create a scatterplot\\n mark: point\\n // Specify where to load data from\\n data: {\\n // By using an object to the url parameter we will\\n // construct an Elasticsearch query\\n url: {\\n // Context == true means filters of the dashboard will be taken into account\\n %context%: true\\n // Specify on which field the time picker should operate\\n %timefield%: timestamp\\n // Specify the index pattern to load data from\\n index: kibana_sample_data_logs\\n // This body will be send to Elasticsearch\'s _search endpoint\\n // You can use everything the ES Query DSL supports here\\n body: {\\n // Set the size to load 10000 documents\\n size: 10000,\\n // Just ask for the fields we actually need for visualization\\n _source: [\\"timestamp\\", \\"bytes\\", \\"extension\\"]\\n }\\n }\\n // Tell Vega, that the array of data will be inside hits.hits of the response\\n // since the result returned from Elasticsearch fill have a format like:\\n // {\\n // hits: {\\n // total: 42000,\\n // max_score: 2,\\n // hits: [\\n // < our individual documents >\\n // ]\\n // }\\n // }\\n format: { property: \\"hits.hits\\" }\\n }\\n // You can do transformation and calculation of the data before drawing it\\n transform: [\\n // Since timestamp is a string value, we need to convert it to a unix timestamp\\n // so that Vega can work on it properly.\\n {\\n // Convert _source.timestamp field to a date\\n calculate: \\"toDate(datum._source[\'timestamp\'])\\"\\n // Store the result in a field named \\"time\\" in the object\\n as: \\"time\\"\\n }\\n ]\\n // Specify what data will be drawn on which axis\\n encoding: {\\n x: {\\n // Draw the time field on the x-axis in temporal mode (i.e. as a time axis)\\n field: time\\n type: temporal\\n // Hide the axis label for the x-axis\\n axis: { title: false }\\n }\\n y: {\\n // Draw the bytes of each document on the y-axis\\n field: _source.bytes\\n // Mark the y-axis as quantitative\\n type: quantitative\\n // Specify the label for this axis\\n axis: { title: \\"Transferred bytes\\" }\\n }\\n color: {\\n // Make the color of each point depend on the _source.extension field\\n field: _source.extension\\n // Treat different values as completely unrelated values to each other.\\n // You could switch this to quantitative if you have a numeric field and\\n // want to create a color scale from one color to another depending on that\\n // field\'s value.\\n type: nominal\\n // Rename the legend title so it won\'t just state: \\"_source.extension\\"\\n legend: { title: \'File type\' }\\n }\\n shape: {\\n // Also make the shape of each point dependent on the extension.\\n field: _source.extension\\n type: nominal\\n }\\n }\\n}"},"aggs":[]}', uiStateJSON: '{}', description: '', version: 1, @@ -166,7 +166,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Logs] Source and Destination Sankey Chart', }), visState: - '{"title":"[Logs] Source and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v3.0.json\\n data: [\\n\\t{\\n \\t// query ES based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: kibana_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"geo.src\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \' → \' + datum.stk2 + \'\\t\' + format(datum.size, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', + '{"title":"[Logs] Source and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query ES based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: kibana_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"geo.src\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \' → \' + datum.stk2 + \'\\t\' + format(datum.size, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js index cbfe2a6ede4f2..377567e47ced8 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.js @@ -18,8 +18,7 @@ */ import _ from 'lodash'; -import * as vega from 'vega-lib'; -import * as vegaLite from 'vega-lite'; +import { vega, vegaLite } from '../lib/vega'; import schemaParser from 'vega-schema-url-parser'; import versionCompare from 'compare-versions'; import { EsQueryParser } from './es_query_parser'; @@ -32,15 +31,13 @@ import { i18n } from '@kbn/i18n'; // Set default single color to match other Kibana visualizations const defaultColor = VISUALIZATION_COLORS[0]; - -const DEFAULT_SCHEMA = 'https://vega.github.io/schema/vega/v3.0.json'; - const locToDirMap = { left: 'row-reverse', right: 'row', top: 'column-reverse', bottom: 'column', }; +const DEFAULT_SCHEMA = 'https://vega.github.io/schema/vega/v5.json'; // If there is no "%type%" parameter, use this parser const DEFAULT_PARSER = 'elasticsearch'; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index a40ef31260b6f..51aa4313a97b5 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -20,8 +20,14 @@ import { cloneDeep } from 'lodash'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; + jest.mock('../services'); +jest.mock('../lib/vega', () => ({ + vega: jest.requireActual('vega'), + vegaLite: jest.requireActual('vega-lite'), +})); + describe(`VegaParser._setDefaultValue`, () => { function check(spec, expected, ...params) { return () => { @@ -146,30 +152,30 @@ describe('VegaParser._parseSchema', () => { test('should warn on no vega version specified', () => { const vp = new VegaParser({}); expect(vp._parseSchema()).toBe(false); - expect(vp.spec).toEqual({ $schema: 'https://vega.github.io/schema/vega/v3.0.json' }); + expect(vp.spec).toEqual({ $schema: 'https://vega.github.io/schema/vega/v5.json' }); expect(vp.warnings).toHaveLength(1); }); test( 'should not warn on current vega version', - check('https://vega.github.io/schema/vega/v4.0.json', false, 0) + check('https://vega.github.io/schema/vega/v5.json', false, 0) ); test( 'should not warn on older vega version', - check('https://vega.github.io/schema/vega/v3.0.json', false, 0) + check('https://vega.github.io/schema/vega/v4.json', false, 0) ); test( 'should warn on vega version too new to be supported', - check('https://vega.github.io/schema/vega/v5.0.json', false, 1) + check('https://vega.github.io/schema/vega/v5.99.json', false, 1) ); test( 'should not warn on current vega-lite version', - check('https://vega.github.io/schema/vega-lite/v2.0.json', true, 0) + check('https://vega.github.io/schema/vega-lite/v4.json', true, 0) ); test( 'should warn on vega-lite version too new to be supported', - check('https://vega.github.io/schema/vega-lite/v3.0.json', true, 1) + check('https://vega.github.io/schema/vega-lite/v5.json', true, 1) ); }); diff --git a/src/plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson index cc60832619290..ace1950f4e909 100644 --- a/src/plugins/vis_type_vega/public/default.spec.hjson +++ b/src/plugins/vis_type_vega/public/default.spec.hjson @@ -6,7 +6,7 @@ Welcome to Vega visualizations. Here you can design your own dataviz from scrat This example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner. */ - $schema: https://vega.github.io/schema/vega-lite/v2.json + $schema: https://vega.github.io/schema/vega-lite/v4.json title: Event counts from all indexes // Define the data source diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js new file mode 100644 index 0000000000000..eed4ecb60e151 --- /dev/null +++ b/src/plugins/vis_type_vega/public/lib/vega.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as vegaLite from 'vega-lite/build-es5/vega-lite'; +import * as vega from 'vega/build-es5/vega'; + +export { vega, vegaLite }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 4f6697de1f29e..069664a9c91c1 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -20,8 +20,7 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import * as vega from 'vega-lib'; -import * as vegaLite from 'vega-lite'; +import { vega, vegaLite } from '../lib/vega'; import { Utils } from '../data_model/utils'; import { VISUALIZATION_COLORS } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 1cc163848fcf0..6908fd13a9ca1 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -17,8 +17,8 @@ * under the License. */ -import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; +import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; import { getEmsTileLayerId, getUISettings, getKibanaMapFactory } from '../services'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index a0b0deb4dac19..e3455b97b7fe2 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -17,7 +17,7 @@ * under the License. */ -import * as vega from 'vega-lib'; +import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; export class VegaView extends VegaBaseView { diff --git a/test/accessibility/services/a11y/a11y.ts b/test/accessibility/services/a11y/a11y.ts index 24f16518bc429..525445c60818a 100644 --- a/test/accessibility/services/a11y/a11y.ts +++ b/test/accessibility/services/a11y/a11y.ts @@ -70,7 +70,12 @@ export function A11yProvider({ getService }: FtrProviderContext) { exclude: ([] as string[]) .concat(excludeTestSubj || []) .map((ts) => [testSubjectToCss(ts)]) - .concat([['.ace_scrollbar']]), + .concat([ + ['.ace_scrollbar'], + [ + '.leaflet-vega-container[role="graphics-document"][aria-roledescription="visualization"]', + ], + ]), }; } diff --git a/x-pack/package.json b/x-pack/package.json index 2513a36f50014..3b8991cb9fe39 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -237,6 +237,7 @@ "cytoscape": "^3.10.0", "d3": "3.5.17", "d3-scale": "1.0.7", + "d3-array": "1.2.4", "dedent": "^0.7.0", "del": "^5.1.0", "dragselect": "1.13.1", diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index b7ea026f80fde..ba3174156d5d4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -434,7 +434,7 @@ Array [ > @@ -3370,7 +3370,7 @@ Array [ > Date: Tue, 16 Jun 2020 12:13:18 +0200 Subject: [PATCH 07/68] [APM] Fix confusing request/minute viz (#69143) --- .../shared/charts/TransactionCharts/index.tsx | 4 +- .../__tests__/chartSelectors.test.ts | 29 +- .../apm/public/selectors/chartSelectors.ts | 7 +- .../__snapshots__/transform.test.ts.snap | 491 +++++++++--------- .../charts/get_timeseries_data/index.ts | 2 + .../get_timeseries_data/transform.test.ts | 29 +- .../charts/get_timeseries_data/transform.ts | 53 +- 7 files changed, 327 insertions(+), 288 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 915b55f29ef80..4821e06419e34 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -24,10 +24,10 @@ import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { ITransactionChartData } from '../../../../selectors/chartSelectors'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { - asInteger, tpmUnit, TimeFormatter, getDurationFormatter, + asDecimal, } from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; @@ -86,7 +86,7 @@ export class TransactionCharts extends Component { public getTPMFormatter = (t: number) => { const { urlParams } = this.props; const unit = tpmUnit(urlParams.transactionType); - return `${asInteger(t)} ${unit}`; + return `${asDecimal(t)} ${unit}`; }; public getTPMTooltipFormatter = (p: Coordinate) => { diff --git a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts index 2f0a30a5019a9..901e6052bbf06 100644 --- a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts +++ b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts @@ -15,6 +15,8 @@ import { warningColor, errorColor, } from '../../utils/httpStatusCodeToColor'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ApmTimeSeriesResponse } from '../../../server/lib/transactions/charts/get_timeseries_data/transform'; describe('chartSelectors', () => { describe('getAnomalyScoreSeries', () => { @@ -98,7 +100,7 @@ describe('chartSelectors', () => { }); describe('getTpmSeries', () => { - const apmTimeseries = { + const apmTimeseries: ApmTimeSeriesResponse = { responseTimes: { avg: [], p95: [], @@ -107,13 +109,14 @@ describe('chartSelectors', () => { tpmBuckets: [ { key: 'HTTP 2xx', + avg: 3.5, dataPoints: [ { x: 0, y: 5 }, - { x: 0, y: 2 }, + { x: 1, y: 2 }, ], }, - { key: 'HTTP 4xx', dataPoints: [{ x: 0, y: 1 }] }, - { key: 'HTTP 5xx', dataPoints: [{ x: 0, y: 0 }] }, + { key: 'HTTP 4xx', avg: 1, dataPoints: [{ x: 0, y: 1 }] }, + { key: 'HTTP 5xx', avg: 0, dataPoints: [{ x: 0, y: 0 }] }, ], overallAvgDuration: 200, }; @@ -125,7 +128,7 @@ describe('chartSelectors', () => { color: successColor, data: [ { x: 0, y: 5 }, - { x: 0, y: 2 }, + { x: 1, y: 2 }, ], legendValue: '3.5 tpm', title: 'HTTP 2xx', @@ -154,7 +157,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorSecondary); }); @@ -166,7 +169,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorSecondary); }); @@ -178,7 +181,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorSecondary); }); @@ -190,7 +193,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorSecondary); }); @@ -202,7 +205,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorDanger); }); @@ -214,7 +217,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorDanger); }); @@ -226,7 +229,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorDanger); }); @@ -238,7 +241,7 @@ describe('chartSelectors', () => { expect( getTpmSeries({ ...apmTimeseries, - tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }], + tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], })[0].color ).toEqual(theme.euiColorDanger); }); diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts index f8aed9dcf6d9f..714d62a703f51 100644 --- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { difference, zipObject } from 'lodash'; -import mean from 'lodash.mean'; import { rgba } from 'polished'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts'; @@ -72,7 +71,6 @@ export function getResponseTimeSeries({ }: TimeSeriesAPIResponse) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; - const formattedDuration = asDuration(overallAvgDuration); const series: TimeSeries[] = [ { @@ -80,7 +78,7 @@ export function getResponseTimeSeries({ defaultMessage: 'Avg.', }), data: avg, - legendValue: formattedDuration, + legendValue: asDuration(overallAvgDuration), type: 'linemark', color: theme.euiColorVis1, }, @@ -171,11 +169,10 @@ export function getTpmSeries( } return tpmBuckets.map((bucket) => { - const average = mean(bucket.dataPoints.map((p) => p.y)); return { title: bucket.key, data: bucket.dataPoints, - legendValue: `${asDecimal(average)} ${tpmUnit(transactionType || '')}`, + legendValue: `${asDecimal(bucket.avg)} ${tpmUnit(transactionType || '')}`, type: 'linemark', color: getColor(bucket.key), }; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap index e99c32d933b0b..fc9edb496784f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap @@ -985,326 +985,327 @@ Object { }, "tpmBuckets": Array [ Object { + "avg": 112708, "dataPoints": Array [ Object { "x": 1528113600000, - "y": 82230, + "y": 8223, }, Object { "x": 1528124400000, - "y": 81460, + "y": 8146, }, Object { "x": 1528135200000, - "y": 82320, + "y": 8232, }, Object { "x": 1528146000000, - "y": 82485, + "y": 8248.5, }, Object { "x": 1528156800000, - "y": 83995, + "y": 8399.5, }, Object { "x": 1528167600000, - "y": 82805, + "y": 8280.5, }, Object { "x": 1528178400000, - "y": 82155, + "y": 8215.5, }, Object { "x": 1528189200000, - "y": 81915, + "y": 8191.5, }, Object { "x": 1528200000000, - "y": 81475, + "y": 8147.5, }, Object { "x": 1528210800000, - "y": 83510, + "y": 8351, }, Object { "x": 1528221600000, - "y": 82345, + "y": 8234.5, }, Object { "x": 1528232400000, - "y": 82330, + "y": 8233, }, Object { "x": 1528243200000, - "y": 82755, + "y": 8275.5, }, Object { "x": 1528254000000, - "y": 83375, + "y": 8337.5, }, Object { "x": 1528264800000, - "y": 82050, + "y": 8205, }, Object { "x": 1528275600000, - "y": 81235, + "y": 8123.5, }, Object { "x": 1528286400000, - "y": 75725, + "y": 7572.5, }, Object { "x": 1528297200000, - "y": 80890, + "y": 8089, }, Object { "x": 1528308000000, - "y": 82650, + "y": 8265, }, Object { "x": 1528318800000, - "y": 81055, + "y": 8105.5, }, Object { "x": 1528329600000, - "y": 82265, + "y": 8226.5, }, Object { "x": 1528340400000, - "y": 82515, + "y": 8251.5, }, Object { "x": 1528351200000, - "y": 83020, + "y": 8302, }, Object { "x": 1528362000000, - "y": 82610, + "y": 8261, }, Object { "x": 1528372800000, - "y": 80820, + "y": 8082, }, Object { "x": 1528383600000, - "y": 82600, + "y": 8260, }, Object { "x": 1528394400000, - "y": 82670, + "y": 8267, }, Object { "x": 1528405200000, - "y": 81555, + "y": 8155.5, }, Object { "x": 1528416000000, - "y": 83350, + "y": 8335, }, Object { "x": 1528426800000, - "y": 80960, + "y": 8096, }, Object { "x": 1528437600000, - "y": 82895, + "y": 8289.5, }, Object { "x": 1528448400000, - "y": 81650, + "y": 8165, }, Object { "x": 1528459200000, - "y": 82825, + "y": 8282.5, }, Object { "x": 1528470000000, - "y": 82715, + "y": 8271.5, }, Object { "x": 1528480800000, - "y": 82460, + "y": 8246, }, Object { "x": 1528491600000, - "y": 82020, + "y": 8202, }, Object { "x": 1528502400000, - "y": 22640, + "y": 2264, }, Object { "x": 1528513200000, - "y": 22785, + "y": 2278.5, }, Object { "x": 1528524000000, - "y": 22830, + "y": 2283, }, Object { "x": 1528534800000, - "y": 22930, + "y": 2293, }, Object { "x": 1528545600000, - "y": 23360, + "y": 2336, }, Object { "x": 1528556400000, - "y": 23425, + "y": 2342.5, }, Object { "x": 1528567200000, - "y": 22605, + "y": 2260.5, }, Object { "x": 1528578000000, - "y": 23060, + "y": 2306, }, Object { "x": 1528588800000, - "y": 22675, + "y": 2267.5, }, Object { "x": 1528599600000, - "y": 23030, + "y": 2303, }, Object { "x": 1528610400000, - "y": 23070, + "y": 2307, }, Object { "x": 1528621200000, - "y": 22535, + "y": 2253.5, }, Object { "x": 1528632000000, - "y": 23055, + "y": 2305.5, }, Object { "x": 1528642800000, - "y": 22935, + "y": 2293.5, }, Object { "x": 1528653600000, - "y": 22910, + "y": 2291, }, Object { "x": 1528664400000, - "y": 23075, + "y": 2307.5, }, Object { "x": 1528675200000, - "y": 81255, + "y": 8125.5, }, Object { "x": 1528686000000, - "y": 84125, + "y": 8412.5, }, Object { "x": 1528696800000, - "y": 81440, + "y": 8144, }, Object { "x": 1528707600000, - "y": 82460, + "y": 8246, }, Object { "x": 1528718400000, - "y": 82170, + "y": 8217, }, Object { "x": 1528729200000, - "y": 85015, + "y": 8501.5, }, Object { "x": 1528740000000, - "y": 81820, + "y": 8182, }, Object { "x": 1528750800000, - "y": 83225, + "y": 8322.5, }, Object { "x": 1528761600000, - "y": 83475, + "y": 8347.5, }, Object { "x": 1528772400000, - "y": 82490, + "y": 8249, }, Object { "x": 1528783200000, - "y": 82940, + "y": 8294, }, Object { "x": 1528794000000, - "y": 83425, + "y": 8342.5, }, Object { "x": 1528804800000, - "y": 81805, + "y": 8180.5, }, Object { "x": 1528815600000, - "y": 83290, + "y": 8329, }, Object { "x": 1528826400000, - "y": 82535, + "y": 8253.5, }, Object { "x": 1528837200000, - "y": 82090, + "y": 8209, }, Object { "x": 1528848000000, - "y": 82385, + "y": 8238.5, }, Object { "x": 1528858800000, - "y": 83775, + "y": 8377.5, }, Object { "x": 1528869600000, - "y": 82970, + "y": 8297, }, Object { "x": 1528880400000, - "y": 84060, + "y": 8406, }, Object { "x": 1528891200000, - "y": 84315, + "y": 8431.5, }, Object { "x": 1528902000000, - "y": 83275, + "y": 8327.5, }, Object { "x": 1528912800000, - "y": 83615, + "y": 8361.5, }, Object { "x": 1528923600000, - "y": 82885, + "y": 8288.5, }, Object { "x": 1528934400000, - "y": 75625, + "y": 7562.5, }, Object { "x": 1528945200000, - "y": 82160, + "y": 8216, }, Object { "x": 1528956000000, - "y": 82320, + "y": 8232, }, Object { "x": 1528966800000, - "y": 81845, + "y": 8184.5, }, Object { "x": 1528977600000, @@ -1314,6 +1315,7 @@ Object { "key": "HTTP 2xx", }, Object { + "avg": 665, "dataPoints": Array [ Object { "x": 1528113600000, @@ -1381,11 +1383,11 @@ Object { }, Object { "x": 1528286400000, - "y": 20205, + "y": 2020.5, }, Object { "x": 1528297200000, - "y": 2270, + "y": 227, }, Object { "x": 1528308000000, @@ -1621,7 +1623,7 @@ Object { }, Object { "x": 1528934400000, - "y": 10775, + "y": 1077.5, }, Object { "x": 1528945200000, @@ -1643,326 +1645,327 @@ Object { "key": "HTTP 3xx", }, Object { + "avg": 8190.7, "dataPoints": Array [ Object { "x": 1528113600000, - "y": 5930, + "y": 593, }, Object { "x": 1528124400000, - "y": 6065, + "y": 606.5, }, Object { "x": 1528135200000, - "y": 6025, + "y": 602.5, }, Object { "x": 1528146000000, - "y": 5810, + "y": 581, }, Object { "x": 1528156800000, - "y": 6190, + "y": 619, }, Object { "x": 1528167600000, - "y": 5955, + "y": 595.5, }, Object { "x": 1528178400000, - "y": 6370, + "y": 637, }, Object { "x": 1528189200000, - "y": 6170, + "y": 617, }, Object { "x": 1528200000000, - "y": 5820, + "y": 582, }, Object { "x": 1528210800000, - "y": 6165, + "y": 616.5, }, Object { "x": 1528221600000, - "y": 6115, + "y": 611.5, }, Object { "x": 1528232400000, - "y": 6080, + "y": 608, }, Object { "x": 1528243200000, - "y": 6000, + "y": 600, }, Object { "x": 1528254000000, - "y": 6185, + "y": 618.5, }, Object { "x": 1528264800000, - "y": 6155, + "y": 615.5, }, Object { "x": 1528275600000, - "y": 5910, + "y": 591, }, Object { "x": 1528286400000, - "y": 5625, + "y": 562.5, }, Object { "x": 1528297200000, - "y": 6215, + "y": 621.5, }, Object { "x": 1528308000000, - "y": 6235, + "y": 623.5, }, Object { "x": 1528318800000, - "y": 5815, + "y": 581.5, }, Object { "x": 1528329600000, - "y": 6100, + "y": 610, }, Object { "x": 1528340400000, - "y": 6010, + "y": 601, }, Object { "x": 1528351200000, - "y": 5960, + "y": 596, }, Object { "x": 1528362000000, - "y": 6240, + "y": 624, }, Object { "x": 1528372800000, - "y": 5945, + "y": 594.5, }, Object { "x": 1528383600000, - "y": 6150, + "y": 615, }, Object { "x": 1528394400000, - "y": 6030, + "y": 603, }, Object { "x": 1528405200000, - "y": 5950, + "y": 595, }, Object { "x": 1528416000000, - "y": 6160, + "y": 616, }, Object { "x": 1528426800000, - "y": 5855, + "y": 585.5, }, Object { "x": 1528437600000, - "y": 6160, + "y": 616, }, Object { "x": 1528448400000, - "y": 6265, + "y": 626.5, }, Object { "x": 1528459200000, - "y": 6250, + "y": 625, }, Object { "x": 1528470000000, - "y": 5835, + "y": 583.5, }, Object { "x": 1528480800000, - "y": 6290, + "y": 629, }, Object { "x": 1528491600000, - "y": 5740, + "y": 574, }, Object { "x": 1528502400000, - "y": 1420, + "y": 142, }, Object { "x": 1528513200000, - "y": 1200, + "y": 120, }, Object { "x": 1528524000000, - "y": 1365, + "y": 136.5, }, Object { "x": 1528534800000, - "y": 1475, + "y": 147.5, }, Object { "x": 1528545600000, - "y": 1405, + "y": 140.5, }, Object { "x": 1528556400000, - "y": 1500, + "y": 150, }, Object { "x": 1528567200000, - "y": 1320, + "y": 132, }, Object { "x": 1528578000000, - "y": 1300, + "y": 130, }, Object { "x": 1528588800000, - "y": 1395, + "y": 139.5, }, Object { "x": 1528599600000, - "y": 1295, + "y": 129.5, }, Object { "x": 1528610400000, - "y": 1455, + "y": 145.5, }, Object { "x": 1528621200000, - "y": 1240, + "y": 124, }, Object { "x": 1528632000000, - "y": 1555, + "y": 155.5, }, Object { "x": 1528642800000, - "y": 1385, + "y": 138.5, }, Object { "x": 1528653600000, - "y": 1395, + "y": 139.5, }, Object { "x": 1528664400000, - "y": 1375, + "y": 137.5, }, Object { "x": 1528675200000, - "y": 5835, + "y": 583.5, }, Object { "x": 1528686000000, - "y": 6350, + "y": 635, }, Object { "x": 1528696800000, - "y": 5815, + "y": 581.5, }, Object { "x": 1528707600000, - "y": 5775, + "y": 577.5, }, Object { "x": 1528718400000, - "y": 6085, + "y": 608.5, }, Object { "x": 1528729200000, - "y": 6135, + "y": 613.5, }, Object { "x": 1528740000000, - "y": 5970, + "y": 597, }, Object { "x": 1528750800000, - "y": 5765, + "y": 576.5, }, Object { "x": 1528761600000, - "y": 6055, + "y": 605.5, }, Object { "x": 1528772400000, - "y": 6015, + "y": 601.5, }, Object { "x": 1528783200000, - "y": 6345, + "y": 634.5, }, Object { "x": 1528794000000, - "y": 5985, + "y": 598.5, }, Object { "x": 1528804800000, - "y": 5920, + "y": 592, }, Object { "x": 1528815600000, - "y": 5880, + "y": 588, }, Object { "x": 1528826400000, - "y": 5810, + "y": 581, }, Object { "x": 1528837200000, - "y": 6350, + "y": 635, }, Object { "x": 1528848000000, - "y": 6120, + "y": 612, }, Object { "x": 1528858800000, - "y": 6275, + "y": 627.5, }, Object { "x": 1528869600000, - "y": 6035, + "y": 603.5, }, Object { "x": 1528880400000, - "y": 6030, + "y": 603, }, Object { "x": 1528891200000, - "y": 6270, + "y": 627, }, Object { "x": 1528902000000, - "y": 6080, + "y": 608, }, Object { "x": 1528912800000, - "y": 6315, + "y": 631.5, }, Object { "x": 1528923600000, - "y": 6385, + "y": 638.5, }, Object { "x": 1528934400000, - "y": 5915, + "y": 591.5, }, Object { "x": 1528945200000, - "y": 6105, + "y": 610.5, }, Object { "x": 1528956000000, - "y": 5990, + "y": 599, }, Object { "x": 1528966800000, - "y": 6070, + "y": 607, }, Object { "x": 1528977600000, @@ -1972,326 +1975,327 @@ Object { "key": "HTTP 4xx", }, Object { + "avg": 8203.6, "dataPoints": Array [ Object { "x": 1528113600000, - "y": 6045, + "y": 604.5, }, Object { "x": 1528124400000, - "y": 6015, + "y": 601.5, }, Object { "x": 1528135200000, - "y": 5980, + "y": 598, }, Object { "x": 1528146000000, - "y": 6150, + "y": 615, }, Object { "x": 1528156800000, - "y": 6165, + "y": 616.5, }, Object { "x": 1528167600000, - "y": 6360, + "y": 636, }, Object { "x": 1528178400000, - "y": 6090, + "y": 609, }, Object { "x": 1528189200000, - "y": 6085, + "y": 608.5, }, Object { "x": 1528200000000, - "y": 6175, + "y": 617.5, }, Object { "x": 1528210800000, - "y": 6245, + "y": 624.5, }, Object { "x": 1528221600000, - "y": 5790, + "y": 579, }, Object { "x": 1528232400000, - "y": 6075, + "y": 607.5, }, Object { "x": 1528243200000, - "y": 5955, + "y": 595.5, }, Object { "x": 1528254000000, - "y": 6175, + "y": 617.5, }, Object { "x": 1528264800000, - "y": 6060, + "y": 606, }, Object { "x": 1528275600000, - "y": 5900, + "y": 590, }, Object { "x": 1528286400000, - "y": 5455, + "y": 545.5, }, Object { "x": 1528297200000, - "y": 5880, + "y": 588, }, Object { "x": 1528308000000, - "y": 6215, + "y": 621.5, }, Object { "x": 1528318800000, - "y": 6040, + "y": 604, }, Object { "x": 1528329600000, - "y": 6010, + "y": 601, }, Object { "x": 1528340400000, - "y": 6440, + "y": 644, }, Object { "x": 1528351200000, - "y": 6205, + "y": 620.5, }, Object { "x": 1528362000000, - "y": 6075, + "y": 607.5, }, Object { "x": 1528372800000, - "y": 5760, + "y": 576, }, Object { "x": 1528383600000, - "y": 6205, + "y": 620.5, }, Object { "x": 1528394400000, - "y": 5885, + "y": 588.5, }, Object { "x": 1528405200000, - "y": 6215, + "y": 621.5, }, Object { "x": 1528416000000, - "y": 6275, + "y": 627.5, }, Object { "x": 1528426800000, - "y": 5945, + "y": 594.5, }, Object { "x": 1528437600000, - "y": 5915, + "y": 591.5, }, Object { "x": 1528448400000, - "y": 6075, + "y": 607.5, }, Object { "x": 1528459200000, - "y": 6410, + "y": 641, }, Object { "x": 1528470000000, - "y": 5885, + "y": 588.5, }, Object { "x": 1528480800000, - "y": 5995, + "y": 599.5, }, Object { "x": 1528491600000, - "y": 6170, + "y": 617, }, Object { "x": 1528502400000, - "y": 1420, + "y": 142, }, Object { "x": 1528513200000, - "y": 1535, + "y": 153.5, }, Object { "x": 1528524000000, - "y": 1415, + "y": 141.5, }, Object { "x": 1528534800000, - "y": 1515, + "y": 151.5, }, Object { "x": 1528545600000, - "y": 1630, + "y": 163, }, Object { "x": 1528556400000, - "y": 1345, + "y": 134.5, }, Object { "x": 1528567200000, - "y": 1485, + "y": 148.5, }, Object { "x": 1528578000000, - "y": 1390, + "y": 139, }, Object { "x": 1528588800000, - "y": 1445, + "y": 144.5, }, Object { "x": 1528599600000, - "y": 1360, + "y": 136, }, Object { "x": 1528610400000, - "y": 1395, + "y": 139.5, }, Object { "x": 1528621200000, - "y": 1190, + "y": 119, }, Object { "x": 1528632000000, - "y": 1440, + "y": 144, }, Object { "x": 1528642800000, - "y": 1290, + "y": 129, }, Object { "x": 1528653600000, - "y": 1320, + "y": 132, }, Object { "x": 1528664400000, - "y": 1480, + "y": 148, }, Object { "x": 1528675200000, - "y": 6065, + "y": 606.5, }, Object { "x": 1528686000000, - "y": 6270, + "y": 627, }, Object { "x": 1528696800000, - "y": 5675, + "y": 567.5, }, Object { "x": 1528707600000, - "y": 6200, + "y": 620, }, Object { "x": 1528718400000, - "y": 6075, + "y": 607.5, }, Object { "x": 1528729200000, - "y": 6195, + "y": 619.5, }, Object { "x": 1528740000000, - "y": 6045, + "y": 604.5, }, Object { "x": 1528750800000, - "y": 6040, + "y": 604, }, Object { "x": 1528761600000, - "y": 5880, + "y": 588, }, Object { "x": 1528772400000, - "y": 6035, + "y": 603.5, }, Object { "x": 1528783200000, - "y": 5990, + "y": 599, }, Object { "x": 1528794000000, - "y": 5825, + "y": 582.5, }, Object { "x": 1528804800000, - "y": 5940, + "y": 594, }, Object { "x": 1528815600000, - "y": 6225, + "y": 622.5, }, Object { "x": 1528826400000, - "y": 6190, + "y": 619, }, Object { "x": 1528837200000, - "y": 6415, + "y": 641.5, }, Object { "x": 1528848000000, - "y": 5990, + "y": 599, }, Object { "x": 1528858800000, - "y": 5860, + "y": 586, }, Object { "x": 1528869600000, - "y": 6145, + "y": 614.5, }, Object { "x": 1528880400000, - "y": 6195, + "y": 619.5, }, Object { "x": 1528891200000, - "y": 6155, + "y": 615.5, }, Object { "x": 1528902000000, - "y": 6240, + "y": 624, }, Object { "x": 1528912800000, - "y": 6100, + "y": 610, }, Object { "x": 1528923600000, - "y": 6120, + "y": 612, }, Object { "x": 1528934400000, - "y": 5440, + "y": 544, }, Object { "x": 1528945200000, - "y": 6175, + "y": 617.5, }, Object { "x": 1528956000000, - "y": 5805, + "y": 580.5, }, Object { "x": 1528966800000, - "y": 5915, + "y": 591.5, }, Object { "x": 1528977600000, @@ -2301,6 +2305,7 @@ Object { "key": "HTTP 5xx", }, Object { + "avg": 0, "dataPoints": Array [], "key": "A Custom Bucket (that should be last)", }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 0f1eca5853bb9..8a0fe1a57736f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -21,10 +21,12 @@ export async function getApmTimeseriesData(options: { }) { const { start, end } = options.setup; const { bucketSize } = getBucketSize(start, end, 'auto'); + const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); return timeseriesTransformer({ timeseriesResponse, bucketSize, + durationAsMinutes, }); } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts index abce5dbe18059..d0d0875be388f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts @@ -16,7 +16,8 @@ describe('timeseriesTransformer', () => { beforeEach(async () => { res = await timeseriesTransformer({ timeseriesResponse, - bucketSize: 12, + bucketSize: 120, + durationAsMinutes: 10, }); }); @@ -79,7 +80,7 @@ describe('getTpmBuckets', () => { { key_as_string: '', key: 1, - doc_count: 500, + doc_count: 100, }, { key_as_string: '', @@ -95,23 +96,31 @@ describe('getTpmBuckets', () => { }, }, ]; - const bucketSize = 10; - expect(getTpmBuckets(buckets as any, bucketSize)).toEqual([ + + expect( + getTpmBuckets({ + transactionResultBuckets: buckets, + bucketSize: 120, + durationAsMinutes: 10, + }) + ).toEqual([ { + avg: 90, dataPoints: [ { x: 0, y: 0 }, - { x: 1, y: 1200 }, - { x: 2, y: 1800 }, - { x: 3, y: 2400 }, + { x: 1, y: 100 }, + { x: 2, y: 150 }, + { x: 3, y: 200 }, ], key: 'HTTP 4xx', }, { + avg: 50, dataPoints: [ { x: 0, y: 0 }, - { x: 1, y: 3000 }, - { x: 2, y: 600 }, - { x: 3, y: 1800 }, + { x: 1, y: 50 }, + { x: 2, y: 50 }, + { x: 3, y: 150 }, ], key: 'HTTP 5xx', }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts index bbdd0bd3e054a..f68c069253b99 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isNumber, round, sortBy } from 'lodash'; +import { isNumber, sortBy } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate } from '../../../../../typings/timeseries'; import { ESResponse } from './fetcher'; @@ -14,16 +14,22 @@ export type ApmTimeSeriesResponse = ReturnType; export function timeseriesTransformer({ timeseriesResponse, bucketSize, + durationAsMinutes, }: { timeseriesResponse: ESResponse; bucketSize: number; + durationAsMinutes: number; }) { const aggs = timeseriesResponse.aggregations; const overallAvgDuration = aggs?.overall_avg_duration.value || null; const responseTimeBuckets = aggs?.response_times.buckets || []; const { avg, p95, p99 } = getResponseTime(responseTimeBuckets); const transactionResultBuckets = aggs?.transaction_results.buckets || []; - const tpmBuckets = getTpmBuckets(transactionResultBuckets, bucketSize); + const tpmBuckets = getTpmBuckets({ + transactionResultBuckets, + bucketSize, + durationAsMinutes, + }); return { responseTimes: { @@ -36,18 +42,28 @@ export function timeseriesTransformer({ }; } -export function getTpmBuckets( - transactionResultBuckets: Required< - ESResponse - >['aggregations']['transaction_results']['buckets'] = [], - bucketSize: number -) { +type TransactionResultBuckets = Required< + ESResponse +>['aggregations']['transaction_results']['buckets']; + +export function getTpmBuckets({ + transactionResultBuckets = [], + bucketSize, + durationAsMinutes, +}: { + transactionResultBuckets: TransactionResultBuckets; + bucketSize: number; + durationAsMinutes: number; +}) { const buckets = transactionResultBuckets.map( ({ key: resultKey, timeseries }) => { const dataPoints = timeseries.buckets.map((bucket) => { + // calculate request/minute. Avoid up-scaling numbers if bucketSize is below 60s (1 minute). + // Eg. 1 request during a 10 second window should be displayed as "1 rpm" instead of "6 rpm". + const tmpValue = bucket.doc_count * (60 / Math.max(60, bucketSize)); return { x: bucket.key, - y: round(bucket.doc_count * (60 / bucketSize), 1), + y: tmpValue, }; }); @@ -55,7 +71,14 @@ export function getTpmBuckets( const key = resultKey === '' ? NOT_AVAILABLE_LABEL : (resultKey as string); - return { key, dataPoints }; + const docCountTotal = timeseries.buckets + .map((bucket) => bucket.doc_count) + .reduce((a, b) => a + b, 0); + + // calculate request/minute + const avg = docCountTotal / durationAsMinutes; + + return { key, dataPoints, avg }; } ); @@ -65,11 +88,11 @@ export function getTpmBuckets( ); } -function getResponseTime( - responseTimeBuckets: Required< - ESResponse - >['aggregations']['response_times']['buckets'] = [] -) { +type ResponseTimeBuckets = Required< + ESResponse +>['aggregations']['response_times']['buckets']; + +function getResponseTime(responseTimeBuckets: ResponseTimeBuckets = []) { return responseTimeBuckets.reduce( (acc, bucket) => { const { '95.0': p95, '99.0': p99 } = bucket.pct.values; From 83370a17b95c61c5c7212c980f000975619d73a5 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 16 Jun 2020 08:22:21 -0400 Subject: [PATCH 08/68] [Ingest pipelines] Fix create pipeline button copy (#69118) --- .../public/application/sections/pipelines_list/table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 97775965f9b45..0803b419bdbe4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -89,7 +89,7 @@ export const PipelineTable: FunctionComponent = ({ {...reactRouterNavigate(history, '/create')} > {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create a pipeline here', + defaultMessage: 'Create a pipeline', })} , ], From 63506834f287c39a36413dfcf544d5222735b5e4 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 16 Jun 2020 08:25:53 -0400 Subject: [PATCH 09/68] [ILM] Update doc link (#68923) --- .../sections/edit_policy/components/min_age_input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js index 28bc8671f29e2..cd690c768a326 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js @@ -188,7 +188,7 @@ export const MinAgeInput = (props) => { errors={errors} helpText={ Date: Tue, 16 Jun 2020 14:03:17 +0100 Subject: [PATCH 10/68] [ML] Model snapshot management (#68182) * [ML] Model snapshot management * more updates * adding calendar range * updating layout * multiple calendars * moving calendar creator * fixing chart issues * fixing chart issues * improving calendar rendering * adding capabilities checks * code clean up * fixing end time argument type * fix translations * code clean up * comments based on review * changes based on review * fixing include * adding useMemo to theme function --- .../ml/common/constants/time_format.ts | 7 + .../types/anomaly_detection_jobs/index.ts | 1 + .../anomaly_detection_jobs/model_snapshot.ts | 21 + .../annotations_table/annotations_table.js | 3 +- .../components/color_range_legend/index.ts | 1 + .../color_range_legend/use_color_range.ts | 12 +- .../components/job_messages/job_messages.tsx | 3 +- .../close_job_confirm/close_job_confirm.tsx | 79 ++++ .../close_job_confirm/index.ts | 7 + .../edit_model_snapshot_flyout.tsx | 218 ++++++++++ .../edit_model_snapshot_flyout/index.ts | 7 + .../components/model_snapshots/index.ts | 7 + .../model_snapshots/model_snapshots_table.tsx | 267 ++++++++++++ .../chart_loader.ts | 87 ++++ .../create_calendar.tsx | 310 +++++++++++++ .../revert_model_snapshot_flyout/index.ts | 7 + .../revert_model_snapshot_flyout.tsx | 409 ++++++++++++++++++ .../forecasts_table/forecasts_table.js | 2 +- .../components/job_details/format_values.js | 2 +- .../components/job_details/job_details.js | 25 +- .../components/jobs_list/jobs_list.js | 2 +- .../jobs_list_view/jobs_list_view.js | 9 +- .../time_range_selector.js | 3 +- .../event_rate_chart/event_rate_chart.tsx | 46 +- .../charts/event_rate_chart/overlay_range.tsx | 83 ++++ .../components/create_result_callout.tsx | 2 +- .../application/services/job_service.js | 7 +- .../services/ml_api_service/index.ts | 47 ++ .../services/ml_api_service/jobs.ts | 33 +- .../edit/events_table/events_table.js | 3 +- .../calendars/edit/events_table/index.js | 2 +- .../edit/new_event_modal/new_event_modal.js | 2 +- .../ml/server/client/elasticsearch_ml.ts | 88 ++++ .../models/calendar/calendar_manager.ts | 26 +- .../server/models/calendar/event_manager.ts | 14 +- .../ml/server/models/calendar/index.ts | 1 + .../ml/server/models/job_service/datafeeds.ts | 4 +- .../ml/server/models/job_service/index.ts | 2 + .../ml/server/models/job_service/jobs.ts | 19 + .../models/job_service/model_snapshots.ts | 102 +++++ .../ml/server/routes/anomaly_detectors.ts | 147 ++++++- .../plugins/ml/server/routes/job_service.ts | 85 ++++ .../schemas/anomaly_detectors_schema.ts | 18 +- .../routes/schemas/job_service_schema.ts | 17 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 46 files changed, 2175 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/time_format.ts create mode 100644 x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx create mode 100644 x-pack/plugins/ml/server/models/job_service/model_snapshots.ts diff --git a/x-pack/plugins/ml/common/constants/time_format.ts b/x-pack/plugins/ml/common/constants/time_format.ts new file mode 100644 index 0000000000000..109dad2d40ac8 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/time_format.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts index 9c299c628426a..deb41fbb832cd 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts @@ -10,3 +10,4 @@ export * from './datafeed'; export * from './datafeed_stats'; export * from './combined_job'; export * from './summary_job'; +export * from './model_snapshot'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts new file mode 100644 index 0000000000000..367a16965a90b --- /dev/null +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JobId } from './job'; +import { ModelSizeStats } from './job_stats'; + +export interface ModelSnapshot { + job_id: JobId; + min_version: string; + timestamp: number; + description: string; + snapshot_id: string; + snapshot_doc_count: number; + model_size_stats: ModelSizeStats; + latest_record_time_stamp: number; + latest_result_time_stamp: number; + retain: boolean; +} diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 52d266cde1a2c..a091da6c359d1 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -43,6 +43,7 @@ import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, } from '../../../../../common/util/job_utils'; +import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { annotation$, @@ -50,8 +51,6 @@ import { annotationsRefreshed, } from '../../../services/annotations_service'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - /** * Table component for rendering the lists of annotations for an ML job. */ diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts index 93a1ec40f1d5e..8c92f47e5aa07 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts @@ -11,4 +11,5 @@ export { useColorRange, COLOR_RANGE, COLOR_RANGE_SCALE, + useCurrentEuiTheme, } from './use_color_range'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 1d5f5cf3a0309..f674372da6785 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -5,7 +5,7 @@ */ import d3 from 'd3'; - +import { useMemo } from 'react'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; @@ -150,7 +150,7 @@ export const useColorRange = ( colorRangeScale = COLOR_RANGE_SCALE.LINEAR, featureCount = 1 ) => { - const euiTheme = useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight; + const { euiTheme } = useCurrentEuiTheme(); const colorRanges: Record = { [COLOR_RANGE.BLUE]: [ @@ -186,3 +186,11 @@ export const useColorRange = ( return scaleTypes[colorRangeScale]; }; + +export function useCurrentEuiTheme() { + const uiSettings = useUiSettings(); + return useMemo( + () => ({ euiTheme: uiSettings.get('theme:darkMode') ? euiThemeDark : euiThemeLight }), + [uiSettings] + ); +} diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index fd2b7902833a6..798ceae0f0732 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -13,10 +13,9 @@ import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { JobMessage } from '../../../../common/types/audit_message'; +import { TIME_FORMAT } from '../../../../common/constants/time_format'; import { JobIcon } from '../job_message_icon'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - interface JobMessagesProps { messages: JobMessage[]; loading: boolean; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx new file mode 100644 index 0000000000000..8716f8c85f208 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; + +import { COMBINED_JOB_STATE } from '../model_snapshots_table'; + +interface Props { + combinedJobState: COMBINED_JOB_STATE; + hideCloseJobModalVisible(): void; + forceCloseJob(): void; +} +export const CloseJobConfirm: FC = ({ + combinedJobState, + hideCloseJobModalVisible, + forceCloseJob, +}) => { + return ( + + +

+ {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( + + )} + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( + + )} +
+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts new file mode 100644 index 0000000000000..195d14160b5e6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CloseJobConfirm } from './close_job_confirm'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx new file mode 100644 index 0000000000000..1e99fd12a9fa6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useCallback, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiTextArea, + EuiFormRow, + EuiSwitch, + EuiConfirmModal, + EuiOverlayMask, + EuiCallOut, +} from '@elastic/eui'; + +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../../services/ml_api_service'; +import { useNotifications } from '../../../contexts/kibana'; + +interface Props { + snapshot: ModelSnapshot; + job: CombinedJobWithStats; + closeFlyout(reload: boolean): void; +} + +export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout }) => { + const { toasts } = useNotifications(); + const [description, setDescription] = useState(snapshot.description); + const [retain, setRetain] = useState(snapshot.retain); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [isCurrentSnapshot, setIsCurrentSnapshot] = useState( + snapshot.snapshot_id === job.model_snapshot_id + ); + + useEffect(() => { + setIsCurrentSnapshot(snapshot.snapshot_id === job.model_snapshot_id); + }, [snapshot]); + + const updateSnapshot = useCallback(async () => { + try { + await ml.updateModelSnapshot(snapshot.job_id, snapshot.snapshot_id, { + description, + retain, + }); + closeWithReload(); + } catch (error) { + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.editModelSnapshotFlyout.saveErrorTitle', { + defaultMessage: 'Model snapshot update failed', + }), + }); + } + }, [retain, description, snapshot]); + + const deleteSnapshot = useCallback(async () => { + try { + await ml.deleteModelSnapshot(snapshot.job_id, snapshot.snapshot_id); + hideDeleteModal(); + closeWithReload(); + } catch (error) { + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.editModelSnapshotFlyout.deleteErrorTitle', { + defaultMessage: 'Model snapshot deletion failed', + }), + }); + } + }, [snapshot]); + + function closeWithReload() { + closeFlyout(true); + } + function closeWithoutReload() { + closeFlyout(false); + } + function showDeleteModal() { + setDeleteModalVisible(true); + } + function hideDeleteModal() { + setDeleteModalVisible(false); + } + + return ( + <> + + + + +
+ +
+
+ + {isCurrentSnapshot && ( + <> + + + + + + )} + + + + + setDescription(e.target.value)} + /> + + + + setRetain(e.target.checked)} + /> + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + {deleteModalVisible && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts new file mode 100644 index 0000000000000..fcb534620e438 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts new file mode 100644 index 0000000000000..e16d69ea3eb83 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ModelSnapshotTable } from './model_snapshots_table'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx new file mode 100644 index 0000000000000..64fdd97903b60 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + EuiLoadingSpinner, + EuiBasicTableColumn, + formatDate, +} from '@elastic/eui'; + +import { checkPermission } from '../../capabilities/check_capabilities'; +import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; +import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; +import { ml } from '../../services/ml_api_service'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; +import { TIME_FORMAT } from '../../../../common/constants/time_format'; +import { CloseJobConfirm } from './close_job_confirm'; +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../common/types/anomaly_detection_jobs'; + +interface Props { + job: CombinedJobWithStats; + refreshJobList: () => void; +} + +export enum COMBINED_JOB_STATE { + OPEN_AND_RUNNING, + OPEN_AND_STOPPED, + CLOSED, + UNKNOWN, +} + +export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { + const canCreateJob = checkPermission('canCreateJob'); + const canStartStopDatafeed = checkPermission('canStartStopDatafeed'); + + const [snapshots, setSnapshots] = useState([]); + const [snapshotsLoaded, setSnapshotsLoaded] = useState(false); + const [editSnapshot, setEditSnapshot] = useState(null); + const [revertSnapshot, setRevertSnapshot] = useState(null); + const [closeJobModalVisible, setCloseJobModalVisible] = useState(null); + const [combinedJobState, setCombinedJobState] = useState(null); + + useEffect(() => { + loadModelSnapshots(); + }, []); + + const loadModelSnapshots = useCallback(async () => { + const { model_snapshots: ms } = await ml.getModelSnapshots(job.job_id); + setSnapshots(ms); + setSnapshotsLoaded(true); + }, [job]); + + const checkJobIsClosed = useCallback( + async (snapshot: ModelSnapshot) => { + const state = await getCombinedJobState(job.job_id); + if (state === COMBINED_JOB_STATE.UNKNOWN) { + // this will only happen if the job has been deleted by another user + // between the time the row has been expended and now + // eslint-disable-next-line no-console + console.error(`Error retrieving state for job ${job.job_id}`); + return; + } + + setCombinedJobState(state); + + if (state === COMBINED_JOB_STATE.CLOSED) { + // show flyout + setRevertSnapshot(snapshot); + } else { + // show close job modal + setCloseJobModalVisible(snapshot); + } + }, + [job] + ); + + function hideCloseJobModalVisible() { + setCombinedJobState(null); + setCloseJobModalVisible(null); + } + + const forceCloseJob = useCallback(async () => { + await ml.jobs.forceStopAndCloseJob(job.job_id); + if (closeJobModalVisible !== null) { + const state = await getCombinedJobState(job.job_id); + if (state === COMBINED_JOB_STATE.CLOSED) { + setRevertSnapshot(closeJobModalVisible); + } + } + hideCloseJobModalVisible(); + }, [job, closeJobModalVisible]); + + const closeEditFlyout = useCallback((reload: boolean) => { + setEditSnapshot(null); + if (reload) { + loadModelSnapshots(); + } + }, []); + + const closeRevertFlyout = useCallback((reload: boolean) => { + setRevertSnapshot(null); + if (reload) { + loadModelSnapshots(); + // wait half a second before refreshing the jobs list + setTimeout(refreshJobList, 500); + } + }, []); + + const columns: Array> = [ + { + field: 'snapshot_id', + name: i18n.translate('xpack.ml.modelSnapshotTable.id', { + defaultMessage: 'ID', + }), + sortable: true, + }, + { + field: 'description', + name: i18n.translate('xpack.ml.modelSnapshotTable.description', { + defaultMessage: 'Description', + }), + sortable: true, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.ml.modelSnapshotTable.time', { + defaultMessage: 'Date created', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'latest_record_time_stamp', + name: i18n.translate('xpack.ml.modelSnapshotTable.latestTimestamp', { + defaultMessage: 'Latest timestamp', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'retain', + name: i18n.translate('xpack.ml.modelSnapshotTable.retain', { + defaultMessage: 'Retain', + }), + width: '100px', + sortable: true, + }, + { + field: '', + width: '100px', + name: i18n.translate('xpack.ml.modelSnapshotTable.actions', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.name', { + defaultMessage: 'Revert', + }), + description: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.description', { + defaultMessage: 'Revert to this snapshot', + }), + enabled: () => canCreateJob && canStartStopDatafeed, + type: 'icon', + icon: 'crosshairs', + onClick: checkJobIsClosed, + }, + { + name: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.name', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.description', { + defaultMessage: 'Edit this snapshot', + }), + enabled: () => canCreateJob, + type: 'icon', + icon: 'pencil', + onClick: setEditSnapshot, + }, + ], + }, + ]; + + if (snapshotsLoaded === false) { + return ( + <> + + + + + + + ); + } + + return ( + <> + + {editSnapshot !== null && ( + + )} + + {revertSnapshot !== null && ( + + )} + + {closeJobModalVisible !== null && combinedJobState !== null && ( + + )} + + ); +}; + +function renderDate(date: number) { + return formatDate(date, TIME_FORMAT); +} + +async function getCombinedJobState(jobId: string) { + const jobs = await ml.jobs.jobs([jobId]); + + if (jobs.length !== 1) { + return COMBINED_JOB_STATE.UNKNOWN; + } + + if (jobs[0].state !== JOB_STATE.CLOSED) { + if (jobs[0].datafeed_config.state !== DATAFEED_STATE.STOPPED) { + return COMBINED_JOB_STATE.OPEN_AND_RUNNING; + } + return COMBINED_JOB_STATE.OPEN_AND_STOPPED; + } + return COMBINED_JOB_STATE.CLOSED; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts new file mode 100644 index 0000000000000..2da1e914b8139 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlResultsService } from '../../../services/results_service'; +import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; +import { getSeverityType } from '../../../../../common/util/anomaly_utils'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; + +export function chartLoaderProvider(mlResultsService: MlResultsService) { + async function loadEventRateForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number + ): Promise { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs + ); + const resp = await mlResultsService.getEventRateData( + job.datafeed_config.indices.join(), + job.datafeed_config.query, + job.data_description.time_field, + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + intervalMs + ); + if (resp.error !== undefined) { + throw resp.error; + } + + const events = Object.entries(resp.results).map(([time, value]) => ({ + time: +time, + value: value as number, + })); + + if (events.length) { + // add one extra bucket with a value of 0 + // so that an extra blank bar gets drawn at the end of the chart + // this solves an issue with elastic charts where the rect annotation + // never covers the last bar. + events.push({ time: events[events.length - 1].time + intervalMs, value: 0 }); + } + + return events; + } + + async function loadAnomalyDataForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number + ) { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs + ); + + const resp = await mlResultsService.getScoresByBucket( + [job.job_id], + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + intervalMs, + 1 + ); + + const results = resp.results[job.job_id]; + if (results === undefined) { + return []; + } + + const anomalies: Record = {}; + anomalies[0] = Object.entries(results).map( + ([time, value]) => + ({ time: +time, value, severity: getSeverityType(value as number) } as Anomaly) + ); + return anomalies; + } + + return { loadEventRateForJob, loadAnomalyDataForJob }; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx new file mode 100644 index 0000000000000..937c394e35bc1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -0,0 +1,310 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useCallback, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import { XYBrushArea } from '@elastic/charts'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiFieldText, + EuiDatePicker, + EuiButtonIcon, + EuiPanel, +} from '@elastic/eui'; + +import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { useCurrentEuiTheme } from '../../../components/color_range_legend'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; + +export interface CalendarEvent { + start: moment.Moment | null; + end: moment.Moment | null; + description: string; +} + +interface Props { + calendarEvents: CalendarEvent[]; + setCalendarEvents: (calendars: CalendarEvent[]) => void; + minSelectableTimeStamp: number; + maxSelectableTimeStamp: number; + eventRateData: LineChartPoint[]; + anomalies: Anomaly[]; + chartReady: boolean; +} + +export const CreateCalendar: FC = ({ + calendarEvents, + setCalendarEvents, + minSelectableTimeStamp, + maxSelectableTimeStamp, + eventRateData, + anomalies, + chartReady, +}) => { + const maxSelectableTimeMoment = moment(maxSelectableTimeStamp); + const minSelectableTimeMoment = moment(minSelectableTimeStamp); + + const { euiTheme } = useCurrentEuiTheme(); + + const onBrushEnd = useCallback( + ({ x }: XYBrushArea) => { + if (x && x.length === 2) { + const end = x[1] < minSelectableTimeStamp ? null : x[1]; + if (end !== null) { + const start = x[0] < minSelectableTimeStamp ? minSelectableTimeStamp : x[0]; + + setCalendarEvents([ + ...calendarEvents, + { + start: moment(start), + end: moment(end), + description: createDefaultEventDescription(calendarEvents.length + 1), + }, + ]); + } + } + }, + [calendarEvents] + ); + + const setStartDate = useCallback( + (start: moment.Moment | null, index: number) => { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start, end: null, description: createDefaultEventDescription(index) }, + ]); + } else { + event.start = start; + setCalendarEvents([...calendarEvents]); + } + }, + [calendarEvents] + ); + + const setEndDate = useCallback( + (end: moment.Moment | null, index: number) => { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start: null, end, description: createDefaultEventDescription(index) }, + ]); + } else { + event.end = end; + setCalendarEvents([...calendarEvents]); + } + }, + [calendarEvents] + ); + + const setDescription = useCallback( + (description: string, index: number) => { + const event = calendarEvents[index]; + if (event !== undefined) { + event.description = description; + setCalendarEvents([...calendarEvents]); + } + }, + [calendarEvents] + ); + + const removeCalendarEvent = useCallback( + (index: number) => { + if (calendarEvents[index] !== undefined) { + const ce = [...calendarEvents]; + ce.splice(index, 1); + setCalendarEvents(ce); + } + }, + [calendarEvents] + ); + + return ( + <> + +
+ +
+ + ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + }))} + onBrushEnd={onBrushEnd} + overlayColor={euiTheme.euiColorPrimary} + /> + + + {calendarEvents.map((c, i) => ( + + + + + + + + setStartDate(d, i)} + /> + + + + + setEndDate(d, i)} + /> + + + + + + + + setDescription(e.target.value, i)} + /> + + + + + + + removeCalendarEvent(i)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.ml.revertModelSnapshotFlyout.createCalendar.deleteLabel', + { + defaultMessage: 'Delete event', + } + )} + /> + + + + + + ))} + + ); +}; + +interface ChartProps { + eventRateData: LineChartPoint[]; + anomalies: Anomaly[]; + loading: boolean; + onBrushEnd(area: XYBrushArea): void; + overlayRanges: Array<{ start: number; end: number }>; + overlayColor: string; +} + +const Chart: FC = memo( + ({ eventRateData, anomalies, loading, onBrushEnd, overlayRanges, overlayColor }) => ( + ({ + start: c.start, + end: c.end, + color: overlayColor, + showMarker: false, + }))} + onBrushEnd={onBrushEnd} + /> + ), + (prev: ChartProps, next: ChartProps) => { + // only redraw if the calendar ranges have changes + return ( + prev.overlayRanges.length === next.overlayRanges.length && + JSON.stringify(prev.overlayRanges) === JSON.stringify(next.overlayRanges) + ); + } +); + +function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { + return event.start !== null && event.end !== null; +} + +function createDefaultEventDescription(index: number) { + return i18n.translate( + 'xpack.ml.revertModelSnapshotFlyout.createCalendar.defaultEventDescription', + { + defaultMessage: 'Auto created event {index}', + values: { index }, + } + ); +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts new file mode 100644 index 0000000000000..33adc65a9e327 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx new file mode 100644 index 0000000000000..ad5915b39d521 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -0,0 +1,409 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import {} from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiFormRow, + EuiSwitch, + EuiConfirmModal, + EuiOverlayMask, + EuiCallOut, + EuiHorizontalRule, + EuiSuperSelect, + EuiText, + formatDate, +} from '@elastic/eui'; + +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../../services/ml_api_service'; +import { useNotifications } from '../../../contexts/kibana'; +import { chartLoaderProvider } from './chart_loader'; +import { mlResultsService } from '../../../services/results_service'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader'; +import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { parseInterval } from '../../../../../common/util/parse_interval'; +import { TIME_FORMAT } from '../../../../../common/constants/time_format'; +import { CreateCalendar, CalendarEvent } from './create_calendar'; + +interface Props { + snapshot: ModelSnapshot; + snapshots: ModelSnapshot[]; + job: CombinedJobWithStats; + closeFlyout(reload: boolean): void; +} + +export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, closeFlyout }) => { + const { toasts } = useNotifications(); + const { loadAnomalyDataForJob, loadEventRateForJob } = useMemo( + () => chartLoaderProvider(mlResultsService), + [] + ); + const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); + const [revertModalVisible, setRevertModalVisible] = useState(false); + const [replay, setReplay] = useState(false); + const [runInRealTime, setRunInRealTime] = useState(false); + const [createCalendar, setCreateCalendar] = useState(false); + const [calendarEvents, setCalendarEvents] = useState([]); + const [calendarEventsValid, setCalendarEventsValid] = useState(true); + + const [eventRateData, setEventRateData] = useState([]); + const [anomalies, setAnomalies] = useState([]); + const [chartReady, setChartReady] = useState(false); + const [applying, setApplying] = useState(false); + + useEffect(() => { + createChartData(); + }, [currentSnapshot]); + + useEffect(() => { + const invalid = calendarEvents.some( + (c) => c.description === '' || c.end === null || c.start === null + ); + setCalendarEventsValid(invalid === false); + + // a bug in elastic charts selection can + // cause duplicate selected areas to be added + // dedupe the calendars based on start and end times + const calMap = new Map( + calendarEvents.map((c) => [`${c.start?.valueOf()}${c.end?.valueOf()}`, c]) + ); + const dedupedCalendarEvents = [...calMap.values()]; + + if (dedupedCalendarEvents.length < calendarEvents.length) { + // deduped list is shorter, we must have removed something. + setCalendarEvents(dedupedCalendarEvents); + } + }, [calendarEvents]); + + const createChartData = useCallback(async () => { + const bucketSpanMs = parseInterval(job.analysis_config.bucket_span)!.asMilliseconds(); + const eventRate = await loadEventRateForJob(job, bucketSpanMs, 100); + const anomalyData = await loadAnomalyDataForJob(job, bucketSpanMs, 100); + setEventRateData(eventRate); + if (anomalyData[0] !== undefined) { + setAnomalies(anomalyData[0]); + } + setChartReady(true); + }, [job]); + + function closeWithReload() { + closeFlyout(true); + } + function closeWithoutReload() { + closeFlyout(false); + } + + function showRevertModal() { + setRevertModalVisible(true); + } + function hideRevertModal() { + setRevertModalVisible(false); + } + + async function applyRevert() { + setApplying(true); + const end = + replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; + try { + const events = + replay && createCalendar + ? calendarEvents.filter(filterIncompleteEvents).map((c) => ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + description: c.description, + })) + : undefined; + + await ml.jobs.revertModelSnapshot( + job.job_id, + currentSnapshot.snapshot_id, + replay, + end, + events + ); + hideRevertModal(); + closeWithReload(); + } catch (error) { + setApplying(false); + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.revertModelSnapshotFlyout.revertErrorTitle', { + defaultMessage: 'Model snapshot revert failed', + }), + }); + } + } + + function onSnapshotChange(ssId: string) { + const ss = snapshots.find((s) => s.snapshot_id === ssId); + if (ss !== undefined) { + setCurrentSnapshot(ss); + } + } + + return ( + <> + + + +
+ +
+
+ + +

{currentSnapshot.description}

+
+
+ + {false && ( // disabled for now + <> + + + + ({ + value: s.snapshot_id, + inputDisplay: s.snapshot_id, + dropdownDisplay: ( + <> + {s.snapshot_id} + +

{s.description}

+
+ + ), + })) + .reverse()} + valueOfSelected={currentSnapshot.snapshot_id} + onChange={onSnapshotChange} + itemLayoutAlign="top" + hasDividers + /> +
+ + + + )} + + + + + + + + + + + + + + setReplay(e.target.checked)} + /> + + + {replay && ( + <> + + setRunInRealTime(e.target.checked)} + /> + + + + setCreateCalendar(e.target.checked)} + /> + + + {createCalendar && ( + + )} + + )} +
+ + + + + + + + + + + + + + + + +
+ + {revertModalVisible && ( + + + + )} + + ); +}; + +function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { + return event.start !== null && event.end !== null; +} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 817715dbf6413..2f40941fd20fe 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -21,6 +21,7 @@ import { import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; +import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; import { mlForecastService } from '../../../../../services/forecast_service'; import { i18n } from '@kbn/i18n'; @@ -31,7 +32,6 @@ import { } from '../../../../../../../common/util/job_utils'; const MAX_FORECASTS = 500; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of forecasts run on an ML job. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 9194f7537cf3d..883ddfca70cd7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -8,8 +8,8 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { toLocaleString } from '../../../../util/string_utils'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATA_FORMAT = '0.0 b'; function formatData(txt) { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 56da4f1e0ff84..9a5cea62cf6ff 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -14,6 +14,7 @@ import { JsonPane } from './json_tab'; import { DatafeedPreviewPane } from './datafeed_preview_tab'; import { AnnotationsTable } from '../../../../components/annotations/annotations_table'; import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout'; +import { ModelSnapshotTable } from '../../../../components/model_snapshots'; import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; @@ -25,7 +26,7 @@ export class JobDetails extends Component { this.state = {}; if (this.props.addYourself) { - this.props.addYourself(props.jobId, this); + this.props.addYourself(props.jobId, (j) => this.updateJob(j)); } } @@ -33,9 +34,8 @@ export class JobDetails extends Component { this.props.removeYourself(this.props.jobId); } - static getDerivedStateFromProps(props) { - const { job, loading } = props; - return { job, loading }; + updateJob(job) { + this.setState({ job }); } render() { @@ -64,8 +64,7 @@ export class JobDetails extends Component { datafeedTimingStats, } = extractJobDetails(job); - const { showFullDetails } = this.props; - + const { showFullDetails, refreshJobList } = this.props; const tabs = [ { id: 'job-settings', @@ -175,6 +174,19 @@ export class JobDetails extends Component { ), }); + + tabs.push({ + id: 'modelSnapshots', + 'data-test-subj': 'mlJobListTab-modelSnapshots', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.modelSnapshotsLabel', { + defaultMessage: 'Model snapshots', + }), + content: ( + + + + ), + }); } return ( @@ -191,4 +203,5 @@ JobDetails.propTypes = { addYourself: PropTypes.func.isRequired, removeYourself: PropTypes.func.isRequired, showFullDetails: PropTypes.bool, + refreshJobList: PropTypes.func, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 0afaca3ec12e1..23b68551ca0f5 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -15,6 +15,7 @@ import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; import { getJobIdUrl } from '../../../../util/get_job_id_url'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,7 +23,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page export class JobsList extends Component { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index bb9e532245d6d..a3b6cb39815a3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -112,6 +112,7 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } else { @@ -121,6 +122,7 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } @@ -143,10 +145,13 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } - this.setState({ itemIdToExpandedRowMap }); + this.setState({ itemIdToExpandedRowMap }, () => { + this.updateFunctions[jobId](job); + }); }); }) .catch((error) => { @@ -254,7 +259,7 @@ export class JobsListView extends Component { ); Object.keys(this.updateFunctions).forEach((j) => { - this.updateFunctions[j].setState({ job: fullJobsList[j] }); + this.updateFunctions[j](fullJobsList[j]); }); jobs.forEach((job) => { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js index 55c87bbc90b10..8cf244c3c7d8a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js @@ -13,8 +13,7 @@ import { EuiDatePicker, EuiFieldText } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; export class TimeRangeSelector extends Component { constructor(props) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 2fb8ea2820b29..bd6fedd4ba21c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -5,13 +5,22 @@ */ import React, { FC } from 'react'; -import { BarSeries, Chart, ScaleType, Settings, TooltipType } from '@elastic/charts'; +import { + HistogramBarSeries, + Chart, + ScaleType, + Settings, + TooltipType, + BrushEndListener, + PartialTheme, +} from '@elastic/charts'; import { Axes } from '../common/axes'; import { LineChartPoint } from '../../../../common/chart_loader'; import { Anomaly } from '../../../../common/results_loader'; import { useChartColors } from '../common/settings'; import { LoadingWrapper } from '../loading_wrapper'; import { Anomalies } from '../common/anomalies'; +import { OverlayRange } from './overlay_range'; interface Props { eventRateChartData: LineChartPoint[]; @@ -21,6 +30,13 @@ interface Props { showAxis?: boolean; loading?: boolean; fadeChart?: boolean; + overlayRanges?: Array<{ + start: number; + end: number; + color: string; + showMarker?: boolean; + }>; + onBrushEnd?: BrushEndListener; } export const EventRateChart: FC = ({ @@ -31,10 +47,16 @@ export const EventRateChart: FC = ({ showAxis, loading = false, fadeChart, + overlayRanges, + onBrushEnd, }) => { const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; + const theme: PartialTheme = { + scales: { histogramPadding: 0.2 }, + }; + return (
= ({ {showAxis === true && } - + {onBrushEnd === undefined ? ( + + ) : ( + + )} + + {overlayRanges && + overlayRanges.map((range, i) => ( + + ))} + - = ({ + overlayKey, + eventRateChartData, + start, + end, + color, + showMarker = true, +}) => { + const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); + + return ( + <> + + +
+
+ +
+
+ {formatDate(start, TIME_FORMAT)} +
+
+ + ) : undefined + } + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx index 9d0cf705aaba6..4602ceeec905f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx @@ -67,7 +67,7 @@ export const CreateResultCallout: FC = memo( color="primary" fill={false} aria-label={i18n.translate( - 'xpack.ml.newJi18n(ob.recognize.jobsCreationFailed.resetButtonAriaLabel', + 'xpack.ml.newJob.recognize.jobsCreationFailed.resetButtonAriaLabel', { defaultMessage: 'Reset' } )} onClick={onReset} diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index a3be479571702..6c0f393c267aa 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -13,6 +13,7 @@ import { ml } from './ml_api_service'; import { mlMessageBarService } from '../components/messagebar'; import { isWebUrl } from '../util/url_utils'; import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; +import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; const msgs = mlMessageBarService; @@ -929,10 +930,8 @@ function createResultsUrlForJobs(jobsList, resultsPage) { } } - const timeFormat = 'YYYY-MM-DD HH:mm:ss'; - - const fromString = moment(from).format(timeFormat); // Defaults to 'now' if 'from' is undefined - const toString = moment(to).format(timeFormat); // Defaults to 'now' if 'to' is undefined + const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined + const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined const jobIds = jobsList.map((j) => j.id); return createResultsUrl(jobIds, fromString, toString, resultsPage); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 6e3fd08e90e38..fdaa3c2ffe79e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -23,6 +23,7 @@ import { CombinedJob, Detector, AnalysisConfig, + ModelSnapshot, } from '../../../../common/types/anomaly_detection_jobs'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { FieldRequestConfig } from '../../datavisualizer/index_based/common'; @@ -77,6 +78,11 @@ export interface CardinalityModelPlotHigh { export type CardinalityValidationResult = SuccessCardinality | CardinalityModelPlotHigh; export type CardinalityValidationResults = CardinalityValidationResult[]; +export interface GetModelSnapshotsResponse { + count: number; + model_snapshots: ModelSnapshot[]; +} + export function basePath() { return '/api/ml'; } @@ -119,6 +125,13 @@ export const ml = { }); }, + forceCloseJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`, + method: 'POST', + }); + }, + deleteJob({ jobId }: { jobId: string }) { return http({ path: `${basePath()}/anomaly_detectors/${jobId}`, @@ -242,6 +255,13 @@ export const ml = { }); }, + forceStopDatafeed({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`, + method: 'POST', + }); + }, + datafeedPreview({ datafeedId }: { datafeedId: string }) { return http({ path: `${basePath()}/datafeeds/${datafeedId}/_preview`, @@ -640,6 +660,33 @@ export const ml = { }); }, + getModelSnapshots(jobId: string, snapshotId?: string) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${ + snapshotId !== undefined ? `/${snapshotId}` : '' + }`, + }); + }, + + updateModelSnapshot( + jobId: string, + snapshotId: string, + body: { description?: string; retain?: boolean } + ) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`, + method: 'POST', + body: JSON.stringify(body), + }); + }, + + deleteModelSnapshot(jobId: string, snapshotId: string) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`, + method: 'DELETE', + }); + }, + annotations, dataFrameAnalytics, filters, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index e2569f6217b34..6aa62da3f0768 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -8,7 +8,11 @@ import { http } from '../http_service'; import { basePath } from './index'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange, MlSummaryJobs } from '../../../../common/types/anomaly_detection_jobs'; +import { + MlJobWithTimeRange, + MlSummaryJobs, + CombinedJobWithStats, +} from '../../../../common/types/anomaly_detection_jobs'; import { JobMessage } from '../../../../common/types/audit_message'; import { AggFieldNamePair } from '../../../../common/types/fields'; import { ExistingJobsAndGroups } from '../job_service'; @@ -41,7 +45,7 @@ export const jobs = { jobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return http({ path: `${basePath()}/jobs/jobs`, method: 'POST', body, @@ -95,6 +99,7 @@ export const jobs = { body, }); }, + closeJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); return http({ @@ -104,6 +109,15 @@ export const jobs = { }); }, + forceStopAndCloseJob(jobId: string) { + const body = JSON.stringify({ jobId }); + return http<{ success: boolean }>({ + path: `${basePath()}/jobs/force_stop_and_close_job`, + method: 'POST', + body, + }); + }, + jobAuditMessages(jobId: string, from?: number) { const jobIdString = jobId !== undefined ? `/${jobId}` : ''; const query = from !== undefined ? { from } : {}; @@ -255,4 +269,19 @@ export const jobs = { body, }); }, + + revertModelSnapshot( + jobId: string, + snapshotId: string, + replay: boolean, + end?: number, + calendarEvents?: Array<{ start: number; end: number; description: string }> + ) { + const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents }); + return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ + path: `${basePath()}/jobs/revert_model_snapshot`, + method: 'POST', + body, + }); + }, }; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index 815aa4810ebaa..9069e8078fca6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -12,8 +12,7 @@ import { EuiButton, EuiButtonEmpty, EuiInMemoryTable, EuiSpacer } from '@elastic import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; function DeleteButton({ onClick, canDeleteCalendar }) { return ( diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js index 0910a04051bf4..89bd0235df996 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EventsTable, TIME_FORMAT } from './events_table'; +export { EventsTable } from './events_table'; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 8380fd36b458c..d80e248674a8f 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -24,7 +24,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import moment from 'moment'; -import { TIME_FORMAT } from '../events_table'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { generateTempId } from '../utils'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts index d5c7882a30d20..07159534e1e2c 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts @@ -393,6 +393,17 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ml.stopDatafeed = ca({ urls: [ + { + fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop?force=<%=force%>', + req: { + datafeedId: { + type: 'string', + }, + force: { + type: 'boolean', + }, + }, + }, { fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop', req: { @@ -823,4 +834,81 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'GET', }); + + ml.modelSnapshots = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots', + req: { + jobId: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ml.updateModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_update', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + needBody: true, + }); + + ml.deleteModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); + + ml.revertModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_revert', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); }; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index acb1bed6a37c0..2eec704f1e784 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,7 @@ */ import { difference } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; +import { APICaller } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,16 +23,16 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: IScopedClusterClient['callAsCurrentUser']; - private _eventManager: any; + private _callAsCurrentUser: APICaller; + private _eventManager: EventManager; - constructor(client: any) { - this._client = client; - this._eventManager = new EventManager(client); + constructor(callAsCurrentUser: APICaller) { + this._callAsCurrentUser = callAsCurrentUser; + this._eventManager = new EventManager(callAsCurrentUser); } async getCalendar(calendarId: string) { - const resp = await this._client('ml.calendars', { + const resp = await this._callAsCurrentUser('ml.calendars', { calendarId, }); @@ -43,7 +43,7 @@ export class CalendarManager { } async getAllCalendars() { - const calendarsResp = await this._client('ml.calendars'); + const calendarsResp = await this._callAsCurrentUser('ml.calendars'); const events: CalendarEvent[] = await this._eventManager.getAllEvents(); const calendars: Calendar[] = calendarsResp.calendars; @@ -74,7 +74,7 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - await this._client('ml.addCalendar', { + await this._callAsCurrentUser('ml.addCalendar', { calendarId, body: calendar, }); @@ -109,7 +109,7 @@ export class CalendarManager { // add all new jobs if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { + await this._callAsCurrentUser('ml.addJobToCalendar', { calendarId, jobId: jobsToAdd.join(','), }); @@ -117,7 +117,7 @@ export class CalendarManager { // remove all removed jobs if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { + await this._callAsCurrentUser('ml.removeJobFromCalendar', { calendarId, jobId: jobsToRemove.join(','), }); @@ -131,7 +131,7 @@ export class CalendarManager { // remove all removed events await Promise.all( eventsToRemove.map(async (event) => { - await this._eventManager.deleteEvent(calendarId, event.event_id); + await this._eventManager.deleteEvent(calendarId, event.event_id!); }) ); @@ -140,6 +140,6 @@ export class CalendarManager { } async deleteCalendar(calendarId: string) { - return this._client('ml.deleteCalendar', { calendarId }); + return this._callAsCurrentUser('ml.deleteCalendar', { calendarId }); } } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 41240e2695f6f..02da0718d83ae 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -15,13 +16,10 @@ export interface CalendarEvent { } export class EventManager { - private _client: any; - constructor(client: any) { - this._client = client; - } + constructor(private _callAsCurrentUser: APICaller) {} async getCalendarEvents(calendarId: string) { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._callAsCurrentUser('ml.events', { calendarId }); return resp.events; } @@ -29,7 +27,7 @@ export class EventManager { // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - const resp = await this._client('ml.events', { + const resp = await this._callAsCurrentUser('ml.events', { calendarId, jobId, }); @@ -40,14 +38,14 @@ export class EventManager { async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - return await this._client('ml.addEvent', { + return await this._callAsCurrentUser('ml.addEvent', { calendarId, body, }); } async deleteEvent(calendarId: string, eventId: string) { - return this._client('ml.deleteEvent', { calendarId, eventId }); + return this._callAsCurrentUser('ml.deleteEvent', { calendarId, eventId }); } isEqual(ev1: CalendarEvent, ev2: CalendarEvent) { diff --git a/x-pack/plugins/ml/server/models/calendar/index.ts b/x-pack/plugins/ml/server/models/calendar/index.ts index 2364c3ac73811..1a35f9f13368e 100644 --- a/x-pack/plugins/ml/server/models/calendar/index.ts +++ b/x-pack/plugins/ml/server/models/calendar/index.ts @@ -5,3 +5,4 @@ */ export { CalendarManager, Calendar, FormCalendar } from './calendar_manager'; +export { CalendarEvent } from './event_manager'; diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 4090a59c461da..f016898075918 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -27,7 +27,7 @@ interface Results { } export function datafeedsProvider(callAsCurrentUser: APICaller) { - async function forceStartDatafeeds(datafeedIds: string[], start: number, end: number) { + async function forceStartDatafeeds(datafeedIds: string[], start?: number, end?: number) { const jobIds = await getJobIdsByDatafeedId(); const doStartsCalled = datafeedIds.reduce((acc, cur) => { acc[cur] = false; @@ -96,7 +96,7 @@ export function datafeedsProvider(callAsCurrentUser: APICaller) { return opened; } - async function startDatafeed(datafeedId: string, start: number, end: number) { + async function startDatafeed(datafeedId: string, start?: number, end?: number) { return callAsCurrentUser('ml.startDatafeed', { datafeedId, start, end }); } diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index eb70a3ccecfc1..b6e87b98735ee 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -10,6 +10,7 @@ import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; import { newJobChartsProvider, topCategoriesProvider } from './new_job'; +import { modelSnapshotProvider } from './model_snapshots'; export function jobServiceProvider(callAsCurrentUser: APICaller) { return { @@ -19,5 +20,6 @@ export function jobServiceProvider(callAsCurrentUser: APICaller) { ...newJobCapsProvider(callAsCurrentUser), ...newJobChartsProvider(callAsCurrentUser), ...topCategoriesProvider(callAsCurrentUser), + ...modelSnapshotProvider(callAsCurrentUser), }; } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 5503169f2d371..852264b3d0337 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; +import Boom from 'boom'; import { APICaller } from 'kibana/server'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { @@ -128,6 +129,23 @@ export function jobsProvider(callAsCurrentUser: APICaller) { return results; } + async function forceStopAndCloseJob(jobId: string) { + const datafeedIds = await getDatafeedIdsByJobId(); + const datafeedId = datafeedIds[jobId]; + if (datafeedId === undefined) { + throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); + } + + const dfResult = await callAsCurrentUser('ml.stopDatafeed', { datafeedId, force: true }); + if (!dfResult || dfResult.stopped !== true) { + return { success: false }; + } + + await callAsCurrentUser('ml.closeJob', { jobId, force: true }); + + return { success: true }; + } + async function jobsSummary(jobIds: string[] = []) { const fullJobsList: CombinedJobWithStats[] = await createFullJobsList(); const fullJobsIds = fullJobsList.map((job) => job.job_id); @@ -472,6 +490,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { forceDeleteJob, deleteJobs, closeJobs, + forceStopAndCloseJob, jobsSummary, jobsWithTimerange, createFullJobsList, diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts new file mode 100644 index 0000000000000..4ffae17fc1c06 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { APICaller } from 'kibana/server'; +import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; +import { datafeedsProvider, MlDatafeedsResponse } from './datafeeds'; +import { MlJobsResponse } from './jobs'; +import { FormCalendar, CalendarManager } from '../calendar'; + +export interface ModelSnapshotsResponse { + count: number; + model_snapshots: ModelSnapshot[]; +} +export interface RevertModelSnapshotResponse { + model: ModelSnapshot; +} + +export function modelSnapshotProvider(callAsCurrentUser: APICaller) { + const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(callAsCurrentUser); + + async function revertModelSnapshot( + jobId: string, + snapshotId: string, + replay: boolean, + end?: number, + deleteInterveningResults: boolean = true, + calendarEvents?: [{ start: number; end: number; description: string }] + ) { + let datafeedId = `datafeed-${jobId}`; + // ensure job exists + await callAsCurrentUser('ml.jobs', { jobId: [jobId] }); + + try { + // ensure the datafeed exists + // the datafeed is probably called datafeed- + await callAsCurrentUser('ml.datafeeds', { + datafeedId: [datafeedId], + }); + } catch (e) { + // if the datafeed isn't called datafeed- + // check all datafeeds to see if one exists that is matched to this job id + const datafeedIds = await getDatafeedIdsByJobId(); + datafeedId = datafeedIds[jobId]; + if (datafeedId === undefined) { + throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); + } + } + + // ensure the snapshot exists + const snapshot = await callAsCurrentUser('ml.modelSnapshots', { + jobId, + snapshotId, + }); + + // apply the snapshot revert + const { model } = await callAsCurrentUser( + 'ml.revertModelSnapshot', + { + jobId, + snapshotId, + body: { + delete_intervening_results: deleteInterveningResults, + }, + } + ); + + // create calendar (if specified) and replay datafeed + if (replay && model.snapshot_id === snapshotId && snapshot.model_snapshots.length) { + // create calendar before starting restarting the datafeed + if (calendarEvents !== undefined && calendarEvents.length) { + const calendar: FormCalendar = { + calendarId: String(Date.now()), + job_ids: [jobId], + description: i18n.translate( + 'xpack.ml.models.jobService.revertModelSnapshot.autoCreatedCalendar.description', + { + defaultMessage: 'Auto created', + } + ), + events: calendarEvents.map((s) => ({ + description: s.description, + start_time: s.start, + end_time: s.end, + })), + }; + const cm = new CalendarManager(callAsCurrentUser); + await cm.newCalendar(calendar); + } + + forceStartDatafeeds([datafeedId], snapshot.model_snapshots[0].latest_record_time_stamp, end); + } + + return { success: true }; + } + + return { revertModelSnapshot }; +} diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 63cd5498231af..0b544d4eca0ed 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -17,6 +17,8 @@ import { getCategoriesSchema, forecastAnomalyDetector, getBucketParamsSchema, + getModelSnapshotsSchema, + updateModelSnapshotSchema, } from './schemas/anomaly_detectors_schema'; /** @@ -526,7 +528,7 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {get} /api/ml/anomaly_detectors/:jobId/results/categories/:categoryId Get results category data by job id and category id + * @api {get} /api/ml/anomaly_detectors/:jobId/results/categories/:categoryId Get results category data by job ID and category ID * @apiName GetCategories * @apiDescription Returns the categories results for the specified job ID and category ID. * @@ -544,11 +546,148 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - const options = { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.categories', { jobId: request.params.jobId, categoryId: request.params.categoryId, - }; - const results = await context.ml!.mlClient.callAsCurrentUser('ml.categories', options); + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots Get model snapshots by job ID + * @apiName GetModelSnapshots + * @apiDescription Returns the model snapshots for the specified job ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.get( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.modelSnapshots', { + jobId: request.params.jobId, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID + * @apiName GetModelSnapshotsById + * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.get( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.modelSnapshots', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {post} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId/_update update model snapshot by snapshot ID + * @apiName UpdateModelSnapshotsById + * @apiDescription Updates the model snapshot for the specified snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + * @apiSchema (body) updateModelSnapshotSchema + */ + router.post( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}/_update', + validate: { + params: getModelSnapshotsSchema, + body: updateModelSnapshotSchema, + }, + options: { + tags: ['access:ml:canCreateJob'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.updateModelSnapshot', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + body: request.body, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {delete} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Delete model snapshots by snapshot ID + * @apiName GetModelSnapshotsById + * @apiDescription Deletes the model snapshot for the specified snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.delete( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canCreateJob'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.deleteModelSnapshot', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + }); return response.ok({ body: results, }); diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 05c44e1da9757..10d1c9952b540 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -17,8 +17,11 @@ import { lookBackProgressSchema, topCategoriesSchema, updateGroupsSchema, + revertModelSnapshotSchema, } from './schemas/job_service_schema'; +import { jobIdSchema } from './schemas/anomaly_detectors_schema'; + import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; @@ -162,6 +165,40 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/force_stop_and_close_job Force stop and close job + * @apiName ForceStopAndCloseJob + * @apiDescription Force stops the datafeed and then force closes the anomaly detection job specified by job ID + * + * @apiSchema (body) jobIdSchema + */ + router.post( + { + path: '/api/ml/jobs/force_stop_and_close_job', + validate: { + body: jobIdSchema, + }, + options: { + tags: ['access:ml:canCloseJob', 'access:ml:canStartStopDatafeed'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { forceStopAndCloseJob } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId } = request.body; + const resp = await forceStopAndCloseJob(jobId); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup JobService * @@ -691,4 +728,52 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { } }) ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/revert_model_snapshot Revert model snapshot + * @apiName RevertModelSnapshot + * @apiDescription Reverts a job to a specified snapshot. Also allows the job to replayed to a specified date and to auto create calendars to skip analysis of specified date ranges + * + * @apiSchema (body) revertModelSnapshotSchema + */ + router.post( + { + path: '/api/ml/jobs/revert_model_snapshot', + validate: { + body: revertModelSnapshotSchema, + }, + options: { + tags: ['access:ml:canCreateJob', 'access:ml:canStartStopDatafeed'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { revertModelSnapshot } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { + jobId, + snapshotId, + replay, + end, + deleteInterveningResults, + calendarEvents, + } = request.body; + const resp = await revertModelSnapshot( + jobId, + snapshotId, + replay, + end, + deleteInterveningResults, + calendarEvents + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 3a01a616a4f7c..16eaab20fe8cb 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -178,10 +178,24 @@ export const getOverallBucketsSchema = schema.object({ }); export const getCategoriesSchema = schema.object({ - /** Category id */ + /** Category ID */ categoryId: schema.string(), - /** Job id */ + /** Job ID */ jobId: schema.string(), }); +export const getModelSnapshotsSchema = schema.object({ + /** Snapshot ID */ + snapshotId: schema.maybe(schema.string()), + /** Job ID */ + jobId: schema.string(), +}); + +export const updateModelSnapshotSchema = schema.object({ + /** description */ + description: schema.maybe(schema.string()), + /** retain */ + retain: schema.maybe(schema.boolean()), +}); + export const forecastAnomalyDetector = schema.object({ duration: schema.any() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index be107db9508fd..36e340adf0b31 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -66,3 +66,20 @@ export const updateGroupsSchema = { ) ), }; + +export const revertModelSnapshotSchema = schema.object({ + jobId: schema.string(), + snapshotId: schema.string(), + replay: schema.boolean(), + end: schema.maybe(schema.number()), + deleteInterveningResults: schema.maybe(schema.boolean()), + calendarEvents: schema.maybe( + schema.arrayOf( + schema.object({ + start: schema.number(), + end: schema.number(), + description: schema.string(), + }) + ) + ), +}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8888120d59f88..dca4224592025 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10455,7 +10455,6 @@ "xpack.ml.navMenu.dataVisualizerTabLinkText": "データビジュアライザー", "xpack.ml.navMenu.overviewTabLinkText": "概要", "xpack.ml.navMenu.settingsTabLinkText": "設定", - "xpack.ml.newJi18n(ob.recognize.jobsCreationFailed.resetButtonAriaLabel": "リセット", "xpack.ml.newJob.page.createJob": "ジョブを作成", "xpack.ml.newJob.recognize.advancedLabel": "高度な設定", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高度な設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3109bab4b9426..2fb11c2252b4e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10459,7 +10459,6 @@ "xpack.ml.navMenu.dataVisualizerTabLinkText": "数据可视化工具", "xpack.ml.navMenu.overviewTabLinkText": "概览", "xpack.ml.navMenu.settingsTabLinkText": "设置", - "xpack.ml.newJi18n(ob.recognize.jobsCreationFailed.resetButtonAriaLabel": "重置", "xpack.ml.newJob.page.createJob": "创建作业", "xpack.ml.newJob.recognize.advancedLabel": "高级", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高级设置", From dd1f64df55b5c92f65f54cdc58415f9f87a55e13 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 16 Jun 2020 08:10:54 -0600 Subject: [PATCH 11/68] [Maps] Security layer wizards (#68290) * [Maps] security layer wizard * index pattern select * use IndexPattern * destination layer * source layer * cache security index patterns * line layer * use top hits * tslint * unit test * fix loading speed and i18n * handle case where ui setting siem:defaultIndex does not exist * tslint * review feedback * extract lazy load bundle from map embeddables * expose createSecurityLayers via plugin start * tslint * export MapsPluginStart * fix path move * review feedback * fix import Co-authored-by: Elastic Machine --- .../api/create_security_layer_descriptors.ts | 16 + x-pack/plugins/maps/public/api/index.ts | 7 + x-pack/plugins/maps/public/api/start_api.ts | 14 + .../classes/layers/load_layer_wizards.ts | 2 + .../observability/create_layer_descriptor.ts | 3 +- .../solution_layers/observability/index.ts | 1 + .../security/create_layer_descriptors.test.ts | 688 ++++++++++++++++++ .../security/create_layer_descriptors.ts | 209 ++++++ .../layers/solution_layers/security/index.ts | 8 + .../security/index_pattern_select.tsx | 96 +++ .../security/security_index_pattern_utils.ts | 44 ++ .../security/security_layer_template.tsx | 55 ++ .../security/security_layer_wizard.tsx | 28 + .../es_pew_pew_source/es_pew_pew_source.js | 3 +- .../flyout_body/layer_wizard_select.tsx | 12 +- x-pack/plugins/maps/public/index.ts | 1 + .../maps/public/lazy_load_bundle/index.ts | 6 + .../public/lazy_load_bundle/lazy/index.ts | 1 + x-pack/plugins/maps/public/plugin.ts | 7 +- 19 files changed, 1197 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/maps/public/api/create_security_layer_descriptors.ts create mode 100644 x-pack/plugins/maps/public/api/index.ts create mode 100644 x-pack/plugins/maps/public/api/start_api.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/index.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/index_pattern_select.tsx create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_template.tsx create mode 100644 x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_wizard.tsx diff --git a/x-pack/plugins/maps/public/api/create_security_layer_descriptors.ts b/x-pack/plugins/maps/public/api/create_security_layer_descriptors.ts new file mode 100644 index 0000000000000..afff92a584b3c --- /dev/null +++ b/x-pack/plugins/maps/public/api/create_security_layer_descriptors.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LayerDescriptor } from '../../common/descriptor_types'; +import { lazyLoadMapModules } from '../lazy_load_bundle'; + +export async function createSecurityLayerDescriptors( + indexPatternId: string, + indexPatternTitle: string +): Promise { + const mapModules = await lazyLoadMapModules(); + return mapModules.createSecurityLayerDescriptors(indexPatternId, indexPatternTitle); +} diff --git a/x-pack/plugins/maps/public/api/index.ts b/x-pack/plugins/maps/public/api/index.ts new file mode 100644 index 0000000000000..8b45d31b41d44 --- /dev/null +++ b/x-pack/plugins/maps/public/api/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MapsStartApi } from './start_api'; diff --git a/x-pack/plugins/maps/public/api/start_api.ts b/x-pack/plugins/maps/public/api/start_api.ts new file mode 100644 index 0000000000000..d45b0df63c839 --- /dev/null +++ b/x-pack/plugins/maps/public/api/start_api.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LayerDescriptor } from '../../common/descriptor_types'; + +export interface MapsStartApi { + createSecurityLayerDescriptors: ( + indexPatternId: string, + indexPatternTitle: string + ) => Promise; +} diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts index 8b6f0a0f3f223..8357971a3778f 100644 --- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -25,6 +25,7 @@ import { tmsLayerWizardConfig } from '../sources/xyz_tms_source'; import { wmsLayerWizardConfig } from '../sources/wms_source'; import { mvtVectorSourceWizardConfig } from '../sources/mvt_single_layer_vector_source'; import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; +import { SecurityLayerWizardConfig } from './solution_layers/security'; import { getEnableVectorTiles } from '../../kibana_services'; let registered = false; @@ -36,6 +37,7 @@ export function registerLayerWizards() { // Registration order determines display order registerLayerWizard(uploadLayerWizardConfig); registerLayerWizard(ObservabilityLayerWizardConfig); + registerLayerWizard(SecurityLayerWizardConfig); // @ts-ignore registerLayerWizard(esDocumentsLayerWizardConfig); // @ts-ignore diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index ba019f97b287f..85601cfc17e8f 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -40,6 +40,7 @@ import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style // redefining APM constant to avoid making maps app depend on APM plugin export const APM_INDEX_PATTERN_ID = 'apm_static_index_pattern_id'; +export const APM_INDEX_PATTERN_TITLE = 'apm-*'; const defaultDynamicProperties = getDefaultDynamicProperties(); @@ -173,7 +174,7 @@ export function createLayerDescriptor({ type: SOURCE_TYPES.ES_TERM_SOURCE, id: joinId, indexPatternId: APM_INDEX_PATTERN_ID, - indexPatternTitle: 'apm-*', // TODO look up from APM_OSS.indexPattern + indexPatternTitle: APM_INDEX_PATTERN_TITLE, // TODO look up from APM_OSS.indexPattern term: 'client.geo.country_iso_code', metrics: [metricsDescriptor], whereQuery: apmSourceQuery, diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts index ae6ade86de980..abe4eccb91e66 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts @@ -5,3 +5,4 @@ */ export { ObservabilityLayerWizardConfig } from './observability_layer_wizard'; +export { APM_INDEX_PATTERN_TITLE } from './create_layer_descriptor'; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts new file mode 100644 index 0000000000000..49a86f45a681b --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts @@ -0,0 +1,688 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../../kibana_services', () => { + const mockUiSettings = { + get: () => { + return undefined; + }, + }; + return { + getUiSettings: () => { + return mockUiSettings; + }, + }; +}); + +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + +import { createSecurityLayerDescriptors } from './create_layer_descriptors'; + +describe('createLayerDescriptor', () => { + test('amp index', () => { + expect(createSecurityLayerDescriptors('id', 'apm-*-transaction*')).toEqual([ + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'apm-*-transaction* | Source Point', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + filterByMapBounds: true, + geoField: 'client.geo.location', + id: '12345', + indexPatternId: 'id', + scalingType: 'TOP_HITS', + sortField: '', + sortOrder: 'desc', + tooltipProperties: [ + 'host.name', + 'client.ip', + 'client.domain', + 'client.geo.country_iso_code', + 'client.as.organization.name', + ], + topHitsSize: 1, + topHitsSplitField: 'client.ip', + type: 'ES_SEARCH', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#6092C0', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'home', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 8, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + size: 2, + }, + type: 'STATIC', + }, + symbolizeAs: { + options: { + value: 'icon', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'apm-*-transaction* | Destination point', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + filterByMapBounds: true, + geoField: 'server.geo.location', + id: '12345', + indexPatternId: 'id', + scalingType: 'TOP_HITS', + sortField: '', + sortOrder: 'desc', + tooltipProperties: [ + 'host.name', + 'server.ip', + 'server.domain', + 'server.geo.country_iso_code', + 'server.as.organization.name', + ], + topHitsSize: 1, + topHitsSplitField: 'server.ip', + type: 'ES_SEARCH', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#D36086', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'marker', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 8, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + size: 2, + }, + type: 'STATIC', + }, + symbolizeAs: { + options: { + value: 'icon', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'apm-*-transaction* | Line', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + destGeoField: 'server.geo.location', + id: '12345', + indexPatternId: 'id', + metrics: [ + { + field: 'client.bytes', + type: 'sum', + }, + { + field: 'server.bytes', + type: 'sum', + }, + ], + sourceGeoField: 'client.geo.location', + type: 'ES_PEW_PEW', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#54B399', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'marker', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 6, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#6092C0', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + field: { + name: 'doc_count', + origin: 'source', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + maxSize: 8, + minSize: 1, + }, + type: 'DYNAMIC', + }, + symbolizeAs: { + options: { + value: 'circle', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + ]); + }); + + test('non-apm index', () => { + expect(createSecurityLayerDescriptors('id', 'filebeat-*')).toEqual([ + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'filebeat-* | Source Point', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + filterByMapBounds: true, + geoField: 'source.geo.location', + id: '12345', + indexPatternId: 'id', + scalingType: 'TOP_HITS', + sortField: '', + sortOrder: 'desc', + tooltipProperties: [ + 'host.name', + 'source.ip', + 'source.domain', + 'source.geo.country_iso_code', + 'source.as.organization.name', + ], + topHitsSize: 1, + topHitsSplitField: 'source.ip', + type: 'ES_SEARCH', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#6092C0', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'home', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 8, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + size: 2, + }, + type: 'STATIC', + }, + symbolizeAs: { + options: { + value: 'icon', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'filebeat-* | Destination point', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + filterByMapBounds: true, + geoField: 'destination.geo.location', + id: '12345', + indexPatternId: 'id', + scalingType: 'TOP_HITS', + sortField: '', + sortOrder: 'desc', + tooltipProperties: [ + 'host.name', + 'destination.ip', + 'destination.domain', + 'destination.geo.country_iso_code', + 'destination.as.organization.name', + ], + topHitsSize: 1, + topHitsSplitField: 'destination.ip', + type: 'ES_SEARCH', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#D36086', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'marker', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 8, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + size: 2, + }, + type: 'STATIC', + }, + symbolizeAs: { + options: { + value: 'icon', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + { + __dataRequests: [], + alpha: 0.75, + id: '12345', + joins: [], + label: 'filebeat-* | Line', + maxZoom: 24, + minZoom: 0, + sourceDescriptor: { + destGeoField: 'destination.geo.location', + id: '12345', + indexPatternId: 'id', + metrics: [ + { + field: 'source.bytes', + type: 'sum', + }, + { + field: 'destination.bytes', + type: 'sum', + }, + ], + sourceGeoField: 'source.geo.location', + type: 'ES_PEW_PEW', + }, + style: { + isTimeAware: true, + properties: { + fillColor: { + options: { + color: '#54B399', + }, + type: 'STATIC', + }, + icon: { + options: { + value: 'marker', + }, + type: 'STATIC', + }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, + iconSize: { + options: { + size: 6, + }, + type: 'STATIC', + }, + labelBorderColor: { + options: { + color: '#FFFFFF', + }, + type: 'STATIC', + }, + labelBorderSize: { + options: { + size: 'SMALL', + }, + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + lineColor: { + options: { + color: '#6092C0', + }, + type: 'STATIC', + }, + lineWidth: { + options: { + field: { + name: 'doc_count', + origin: 'source', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + maxSize: 8, + minSize: 1, + }, + type: 'DYNAMIC', + }, + symbolizeAs: { + options: { + value: 'circle', + }, + }, + }, + type: 'VECTOR', + }, + type: 'VECTOR', + visible: true, + }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts new file mode 100644 index 0000000000000..909cd93b3df7a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import minimatch from 'minimatch'; +import { i18n } from '@kbn/i18n'; +import { euiPaletteColorBlind } from '@elastic/eui'; +import { + LayerDescriptor, + SizeDynamicOptions, + VectorStylePropertiesDescriptor, +} from '../../../../../common/descriptor_types'; +import { + AGG_TYPE, + COUNT_PROP_NAME, + FIELD_ORIGIN, + SCALING_TYPES, + STYLE_TYPE, + SYMBOLIZE_AS_TYPES, + VECTOR_STYLES, +} from '../../../../../common/constants'; +import { VectorLayer } from '../../vector_layer/vector_layer'; +import { VectorStyle } from '../../../styles/vector/vector_style'; +// @ts-ignore +import { ESSearchSource } from '../../../sources/es_search_source'; +// @ts-ignore +import { ESPewPewSource } from '../../../sources/es_pew_pew_source'; +import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults'; +import { APM_INDEX_PATTERN_TITLE } from '../observability'; + +const defaultDynamicProperties = getDefaultDynamicProperties(); +const euiVisColorPalette = euiPaletteColorBlind(); + +function isApmIndex(indexPatternTitle: string) { + return minimatch(indexPatternTitle, APM_INDEX_PATTERN_TITLE); +} + +function getSourceField(indexPatternTitle: string) { + return isApmIndex(indexPatternTitle) ? 'client.geo.location' : 'source.geo.location'; +} + +function getDestinationField(indexPatternTitle: string) { + return isApmIndex(indexPatternTitle) ? 'server.geo.location' : 'destination.geo.location'; +} + +function createSourceLayerDescriptor(indexPatternId: string, indexPatternTitle: string) { + const sourceDescriptor = ESSearchSource.createDescriptor({ + indexPatternId, + geoField: getSourceField(indexPatternTitle), + scalingType: SCALING_TYPES.TOP_HITS, + topHitsSplitField: isApmIndex(indexPatternTitle) ? 'client.ip' : 'source.ip', + tooltipProperties: isApmIndex(indexPatternTitle) + ? [ + 'host.name', + 'client.ip', + 'client.domain', + 'client.geo.country_iso_code', + 'client.as.organization.name', + ] + : [ + 'host.name', + 'source.ip', + 'source.domain', + 'source.geo.country_iso_code', + 'source.as.organization.name', + ], + }); + + const styleProperties: VectorStylePropertiesDescriptor = { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { color: euiVisColorPalette[1] }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { color: '#FFFFFF' }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { type: STYLE_TYPE.STATIC, options: { size: 2 } }, + [VECTOR_STYLES.SYMBOLIZE_AS]: { + options: { value: SYMBOLIZE_AS_TYPES.ICON }, + }, + [VECTOR_STYLES.ICON]: { + type: STYLE_TYPE.STATIC, + options: { value: 'home' }, + }, + [VECTOR_STYLES.ICON_SIZE]: { type: STYLE_TYPE.STATIC, options: { size: 8 } }, + }; + + return VectorLayer.createDescriptor({ + label: i18n.translate('xpack.maps.sescurity.sourceLayerLabel', { + defaultMessage: '{indexPatternTitle} | Source Point', + values: { indexPatternTitle }, + }), + sourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }); +} + +function createDestinationLayerDescriptor(indexPatternId: string, indexPatternTitle: string) { + const sourceDescriptor = ESSearchSource.createDescriptor({ + indexPatternId, + geoField: getDestinationField(indexPatternTitle), + scalingType: SCALING_TYPES.TOP_HITS, + topHitsSplitField: isApmIndex(indexPatternTitle) ? 'server.ip' : 'destination.ip', + tooltipProperties: isApmIndex(indexPatternTitle) + ? [ + 'host.name', + 'server.ip', + 'server.domain', + 'server.geo.country_iso_code', + 'server.as.organization.name', + ] + : [ + 'host.name', + 'destination.ip', + 'destination.domain', + 'destination.geo.country_iso_code', + 'destination.as.organization.name', + ], + }); + + const styleProperties: VectorStylePropertiesDescriptor = { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { color: euiVisColorPalette[2] }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { color: '#FFFFFF' }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { type: STYLE_TYPE.STATIC, options: { size: 2 } }, + [VECTOR_STYLES.SYMBOLIZE_AS]: { + options: { value: SYMBOLIZE_AS_TYPES.ICON }, + }, + [VECTOR_STYLES.ICON]: { + type: STYLE_TYPE.STATIC, + options: { value: 'marker' }, + }, + [VECTOR_STYLES.ICON_SIZE]: { type: STYLE_TYPE.STATIC, options: { size: 8 } }, + }; + + return VectorLayer.createDescriptor({ + label: i18n.translate('xpack.maps.sescurity.destinationLayerLabel', { + defaultMessage: '{indexPatternTitle} | Destination point', + values: { indexPatternTitle }, + }), + sourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }); +} + +function createLineLayerDescriptor(indexPatternId: string, indexPatternTitle: string) { + const sourceDescriptor = ESPewPewSource.createDescriptor({ + indexPatternId, + sourceGeoField: getSourceField(indexPatternTitle), + destGeoField: getDestinationField(indexPatternTitle), + metrics: [ + { + type: AGG_TYPE.SUM, + field: isApmIndex(indexPatternTitle) ? 'client.bytes' : 'source.bytes', + }, + { + type: AGG_TYPE.SUM, + field: isApmIndex(indexPatternTitle) ? 'server.bytes' : 'destination.bytes', + }, + ], + }); + + const styleProperties: VectorStylePropertiesDescriptor = { + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { color: euiVisColorPalette[1] }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH]!.options as SizeDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + minSize: 1, + maxSize: 8, + }, + }, + }; + + return VectorLayer.createDescriptor({ + label: i18n.translate('xpack.maps.sescurity.lineLayerLabel', { + defaultMessage: '{indexPatternTitle} | Line', + values: { indexPatternTitle }, + }), + sourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }); +} + +export function createSecurityLayerDescriptors( + indexPatternId: string, + indexPatternTitle: string +): LayerDescriptor[] { + return [ + createSourceLayerDescriptor(indexPatternId, indexPatternTitle), + createDestinationLayerDescriptor(indexPatternId, indexPatternTitle), + createLineLayerDescriptor(indexPatternId, indexPatternTitle), + ]; +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index.ts new file mode 100644 index 0000000000000..f0cc4f26fc1dd --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSecurityLayerDescriptors } from './create_layer_descriptors'; +export { SecurityLayerWizardConfig } from './security_layer_wizard'; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index_pattern_select.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index_pattern_select.tsx new file mode 100644 index 0000000000000..1e4d9a57a336c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/index_pattern_select.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ChangeEvent, Component } from 'react'; +import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getSecurityIndexPatterns, IndexPatternMeta } from './security_index_pattern_utils'; + +interface Props { + value: string; + onChange: (indexPatternMeta: IndexPatternMeta | null) => void; +} + +interface State { + hasLoaded: boolean; + options: EuiSelectOption[]; +} + +export class IndexPatternSelect extends Component { + private _isMounted: boolean = false; + + state = { + hasLoaded: false, + options: [], + }; + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + this._loadOptions(); + } + + async _loadOptions() { + const indexPatterns = await getSecurityIndexPatterns(); + if (!this._isMounted) { + return; + } + + this.setState({ + hasLoaded: true, + options: [ + { value: '', text: '' }, + ...indexPatterns.map(({ id, title }: IndexPatternMeta) => { + return { + value: id, + text: title, + }; + }), + ], + }); + } + + _onChange = (event: ChangeEvent) => { + const targetOption = this.state.options.find(({ value, text }: EuiSelectOption) => { + return event.target.value === value; + }); + + if (event.target.value === '' || !targetOption) { + this.props.onChange(null); + return; + } + + this.props.onChange({ + // @ts-expect-error - avoid wrong "Property does not exist on type 'never'." compile error + id: targetOption.value, + // @ts-expect-error - avoid wrong "Property does not exist on type 'never'." compile error + title: targetOption.text, + }); + }; + + render() { + if (!this.state.hasLoaded) { + return null; + } + + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts new file mode 100644 index 0000000000000..141b9133505b7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import minimatch from 'minimatch'; +import { SimpleSavedObject } from 'src/core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IndexPatternSavedObjectAttrs } from 'src/plugins/data/common/index_patterns/index_patterns/index_patterns'; +import { getIndexPatternService, getUiSettings } from '../../../../kibana_services'; + +export type IndexPatternMeta = { + id: string; + title: string; +}; + +export async function getSecurityIndexPatterns(): Promise { + const uiSettings = getUiSettings(); + let securityIndexPatternTitles: string[]; + try { + securityIndexPatternTitles = uiSettings.get('securitySolution:defaultIndex'); + } catch (error) { + // UiSettings throws with unreconized configuration setting + // siem:defaultIndex configuration setting is not registered if security app is not running + return []; + } + + const indexPatternCache = await getIndexPatternService().getCache(); + return indexPatternCache! + .filter((savedObject: SimpleSavedObject) => { + return (securityIndexPatternTitles as string[]).some((indexPatternTitle) => { + // glob matching index pattern title + return minimatch(indexPatternTitle, savedObject?.attributes?.title); + }); + }) + .map((savedObject: SimpleSavedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + }; + }); +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_template.tsx new file mode 100644 index 0000000000000..eda489c88fda2 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_template.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { RenderWizardArguments } from '../../layer_wizard_registry'; +import { IndexPatternSelect } from './index_pattern_select'; +import { createSecurityLayerDescriptors } from './create_layer_descriptors'; +import { IndexPatternMeta } from './security_index_pattern_utils'; + +interface State { + indexPatternId: string | null; + indexPatternTitle: string | null; +} + +export class SecurityLayerTemplate extends Component { + state = { + indexPatternId: null, + indexPatternTitle: null, + }; + + _onIndexPatternChange = (indexPatternMeta: IndexPatternMeta | null) => { + this.setState( + { + indexPatternId: indexPatternMeta ? indexPatternMeta.id : null, + indexPatternTitle: indexPatternMeta ? indexPatternMeta.title : null, + }, + this._previewLayer + ); + }; + + _previewLayer() { + if (!this.state.indexPatternId || !this.state.indexPatternTitle) { + this.props.previewLayers([]); + return; + } + + this.props.previewLayers( + createSecurityLayerDescriptors(this.state.indexPatternId!, this.state.indexPatternTitle!) + ); + } + + render() { + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_wizard.tsx new file mode 100644 index 0000000000000..cece00fa37350 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_layer_wizard.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { getSecurityIndexPatterns } from './security_index_pattern_utils'; +import { SecurityLayerTemplate } from './security_layer_template'; + +export const SecurityLayerWizardConfig: LayerWizard = { + checkVisibility: async () => { + const indexPatterns = await getSecurityIndexPatterns(); + return indexPatterns.length > 0; + }, + description: i18n.translate('xpack.maps.security.desc', { + defaultMessage: 'Security layers', + }), + icon: 'logoSecurity', + renderWizard: (renderWizardArguments: RenderWizardArguments) => { + return ; + }, + title: i18n.translate('xpack.maps.security.title', { + defaultMessage: 'Security', + }), +}; diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 0d15cff032410..fda73bc0f73a0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -26,13 +26,14 @@ export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { export class ESPewPewSource extends AbstractESAggSource { static type = SOURCE_TYPES.ES_PEW_PEW; - static createDescriptor({ indexPatternId, sourceGeoField, destGeoField }) { + static createDescriptor({ indexPatternId, sourceGeoField, destGeoField, metrics }) { return { type: ESPewPewSource.type, id: uuid(), indexPatternId: indexPatternId, sourceGeoField, destGeoField, + metrics: metrics ? metrics : [], }; } diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx index 2b1bbfa81c743..b0c50133ceabb 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx @@ -7,6 +7,7 @@ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; +import { EuiLoadingContent } from '@elastic/eui'; import { getLayerWizards, LayerWizard } from '../../../classes/layers/layer_wizard_registry'; interface Props { @@ -15,6 +16,7 @@ interface Props { interface State { layerWizards: LayerWizard[]; + hasLoadedWizards: boolean; } export class LayerWizardSelect extends Component { @@ -22,6 +24,7 @@ export class LayerWizardSelect extends Component { state = { layerWizards: [], + hasLoadedWizards: false, }; componentDidMount() { @@ -36,11 +39,18 @@ export class LayerWizardSelect extends Component { async _loadLayerWizards() { const layerWizards = await getLayerWizards(); if (this._isMounted) { - this.setState({ layerWizards }); + this.setState({ layerWizards, hasLoadedWizards: true }); } } render() { + if (!this.state.hasLoadedWizards) { + return ( +
+ } layout="horizontal" /> +
+ ); + } return this.state.layerWizards.map((layerWizard: LayerWizard) => { const icon = layerWizard.icon ? : undefined; diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 9428946bb62e0..6a144e84b05e0 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -18,3 +18,4 @@ export const plugin: PluginInitializer = ( export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; export { ITooltipProperty } from './classes/tooltips/tooltip_property'; +export { MapsPluginStart } from './plugin'; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 92cefd76aa047..152412376fb09 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -36,6 +36,10 @@ interface LazyLoadedMapModules { initialLayers?: LayerDescriptor[] ) => LayerDescriptor[]; mergeInputWithSavedMap: any; + createSecurityLayerDescriptors: ( + indexPatternId: string, + indexPatternTitle: string + ) => LayerDescriptor[]; } export async function lazyLoadMapModules(): Promise { @@ -56,6 +60,7 @@ export async function lazyLoadMapModules(): Promise { addLayerWithoutDataSync, getInitialLayers, mergeInputWithSavedMap, + createSecurityLayerDescriptors, } = await import('./lazy'); resolve({ @@ -69,6 +74,7 @@ export async function lazyLoadMapModules(): Promise { addLayerWithoutDataSync, getInitialLayers, mergeInputWithSavedMap, + createSecurityLayerDescriptors, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index b650678b3105c..0600b8d5073c6 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -16,3 +16,4 @@ export * from '../../actions'; export * from '../../selectors/map_selectors'; export * from '../../angular/get_initial_layers'; export * from '../../embeddable/merge_input_with_saved_map'; +export * from '../../classes/layers/solution_layers/security'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 319be46870ebc..e0639c9d0f365 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -46,6 +46,8 @@ import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; import { MapsConfigType, MapsXPackConfig } from '../config'; import { ILicense } from '../../licensing/common/types'; +import { MapsStartApi } from './api'; +import { createSecurityLayerDescriptors } from './api/create_security_layer_descriptors'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -143,7 +145,10 @@ export class MapsPlugin }; } - public start(core: CoreStart, plugins: any) { + public start(core: CoreStart, plugins: any): MapsStartApi { bindStartCoreAndPlugins(core, plugins); + return { + createSecurityLayerDescriptors, + }; } } From 7511a6eeb66391aec36dcdfd004c8b058270357e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 16 Jun 2020 16:17:30 +0200 Subject: [PATCH 12/68] [Ingest Node Pipelines] Pipeline Processors Editor (#66021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial plugin setup * add smoke test * fix license check * refactor plugin setup * Server-side create/update ingest pipelines (#62744) * List pipelines (#62785) * First iteration of ingest table * Add action placeholders * Refactor list and address PR feedback Refactored the list into smaller pieces and assemble in main.tsx Also addressed feedback on copy, removed unused notifications dep * WiP on flyout Showing name in title * Add reload button * Finish first version of flyout * Slight update to copy * `delete` -> `edit` * Address PR feedback Copy and a11y updates * Add on failure JSON to flyout if it is available * Add details json block file and remove ununsed import Co-authored-by: Elastic Machine * [Ingest pipelines] Create pipeline UI (#63017) * First vertical slice of pipeline editor component * Made a space for common parameters * [Ingest pipelines] Edit pipeline page (#63522) * First iteration of CRUD functionality working * WiP on moving the pipeline editor to pipeline processor editor * Finish refactor to work with passing state out * Refactor and fix tests * [Ingest pipelines] Polish details panel and empty list (#63926) * Address some early feedback and use FormDataProvider FormDataProvider gives a more declarative way to listen to form state updates. Also refactored the state reader mechanism. * [Ingest pipelines] Delete pipeline (#63635) * [Ingest Node Pipelines] Clone Pipeline (#64049) * First iteration of clone functionality Wired up for both the list table and the details flyout in the list section. * satisfy eslint * Turn on sorting for the list table * Clean up const declarations * Address PR feedback Sentence-casify and update some other copy. * Mark edit and delete as primary actions in list table * Handle URI encoded chars in pipeline name when cloning * Update to using the more flexible controlled component pattern To make this component and mappings editor more consistent, both will expose a more traditional controlled component interface to consumers. In the current implementation this requires that consumers use `useCallback` for `onUpdate` handlers to avoid an infinite rendering cycle and to avoid staleness bugs in their `onUpdate` handlers they need to think about what might make the handler stale. This approach comes with the benefits and flexibility of controlled components at the cost of slightly more complex consumption of the components. In future, we can explore adding the uncontrolled component interface too so that consumers have the option to more simply render and pull data out only when needed * Handle sub-form validity The pipelines processor editor not emits overall validity to consumers * Fix Jest test * Refactor some names prepareDataOut -> serialize prepareDataIn -> deserialize EditorProcessor -> ProcessorInternal * Mark as private * Major WiP Started working on the drag-and-drop-tree and updated some typings stuff * [Ingest node pipelines] Privileges (#63850) * Create privileges check for ingest pipelines app Also moved the public side logic for checking and rendering privilege related messages to es_ui_shared/public following the new __packages_do_not_import__ convention. * Add , * Fix import paths * Address PR feedback Fix i18n strings (remove reference to snapshot and restore) and fix copy referencing snapshot and restore - all copy-pasta errors. Also remove unused field from missing privileges object. * Fix issue from resolving merge conflicts * Add missing app privilege * Use non-deprecated privilige name * First iteration of drag and drop tree on feature parity * First steps toward add on failure handler Updated reducer logic to create the next copy value using immer. * First iteration of nested tree with add on failure working * Refactor and some UI layout updates - Remove the "id" field on processors for now - Implement the nested remove and update functionality again * Remove immer (not call stack safe) Refac to remove immer and reimplemented the immutable set and get functions. * [Ingest Node Pipelines] More lenient treatment of on-failure value (#64411) * Move file to components folder * [Ingest pipelines] Simulate pipeline (#64223) * Updated tree rendering - turn off dropzones for children - fixed up some padding and margins - fixed integration with pipeline_form.tsx The current implementation still has a lot of jank stemming from the UX with DnD. Unfortunately the nesting has opened a number of issues here. * [Ingest Node Pipelines] Show flyout after editing or creating a pipeline (#64409) * Show flyout after editing or creating a pipeline * JSX comment * Show not found flyout Copied from CCR * update not found flyout and fix behavior when viewing details from table * Reset pipeline name in URI when closing flyout * Remove encodeURI Already using encodingURIComponent for unsafe string. Co-authored-by: Alison Goryachev * Clarification of terms - addProcessor -> addTopLevelProcessor (same in editor modes) - Expanded comment on ProcessorSelector * Implement move between lists functionality Added tests to the reducer for moving in and out of a recusrsive structure. * fix TS * Prevent nesting a parent inside of its own child * Add comment * [Ingest pipelines] Cleanup (#64794) * address review feedback * remove unused import * Big refactor to tree rendering structure DnD tree now converts the nested tree to a flat one and only consists of _1_ droppable area with a flat array of draggables that can be combined. Using the existing logic in the reducer combined with translating the flat structure changes to a format the nested reducer can understand looks like a really promising avenue. There still seems to be a bug with a longer list where items do not interact properly. * Remove unused component * A number of NB changes - Fixed a subtle serialisation bug with on_failure in options - Fixed a performance bug by using useRef in pipeline form - Memoized the drag and drop tree - Removed use of "isValid" from form lib... I think this should be removed entirely. * fix bad conflict resolution * Implemented a slightly better destination resolution algo. Also added tests. * Fix subtle staleness bug, whitelist keys for setValue * NB styling fix!! Due to a parent's setting of overflow: hidden the drag and drop tree had a dead zone that would equal the page overflow limit, unsetting on the component itself (overflow: auto) relaxes this limitation. * Fix stale delete bug too * Update naming of editor modes and update comments * Use field types combo box * Add delete confirmation modal * Refactor delete modal component file name * Better visual integration with existing form * Update layout and styling of form - added some padding around the new processors editor field - Updated use of flex box in the pipeline form fields * Move pipeline processor copy into pipeline processor component The test button is also now inside of the pipeline processor editor. Eventually all of this functionality should be moved into the pipeline processor editor. * First step of refactor to moving between trees * First iteration of x-tree drag and drop * Remove unused import * Fix jest test types * Fix up minor i18n issues and fix up layout of on failure * Remove unnecessary prop * Update spacing above add processor button to make it more center * Fix destination resolution algo * Update dragging resolver unit tests and add a lot more comments * Use one sorting algo (removed use of euiReorder for now) * Add placeholder tests and update comments * Quite a big refactor - Remove DraggableLocation entirely, only use ProcessorSelector this simplifies mapping back to reducer instructions quite a lot - Add tests for special case dragging behaviour * Fix off by one bug in tests and implementation 🤦🏼‍♂ - Also move processor reducer to it's own folder * Update behaviour for dragging up across trees and add tests * Fix combine instruction * Fix test and i18n issues * Remove background color * Fix selector after selector refactor * A major performance - Do not re-render the entire tree when editing settings * Fix component smoke test * Fix reading value from processor state using processor selector * [Ingest pipelines] Custom processor form (#66022) * Re add background color and refactor name of processor item * Fix file naming and refactor 🚜 dnd tree rendering - deserialze and serialize were backwards, fixed - the dnd tree was rendering a flat tree which needed to be mapped back to the nested tree for updates. This is still the case but we do use recursive rendering. This enables tree nodes to better hold local state like whether they are collapsed or not. * Fix getting of initial on failure processors value * Update padding styles for containers * A lot of styling updates to get closer to look of mockup * A WiP version of th click-tree an alternative to dnd As a response to really long pipelines we may pivot away from dnd. * Remove dnd tree * clean up reamining dnd tree references * Clean up and refactor of tree component To simplify the logic of the tree component processor id's were added. Also had to update the jest spec to support this * Added the ability to duplicate a processor * Fix types in test * Added duplicate functionality to ui * Memoize tree components * address es lint issues * remove unused import * Fix editing of custom json * Address form performance issues * Add all known missing processors * Add ability to cancel move * Fix staleness in test and view request flyouts * fix type issue * Remove unused translations and skip funcitonal test for now * add todo comment * Fix type issues * remove poc styles * disable other move buttons if we have a selected processor * Refactor drop zone pin to button and add some styling * Refactor processor editor item * Update styling and use icon for cancel move action too * fix nasty integration bug * some minor optimizations * prevent parent from being placed in own on failure handler * Re-add cancel button * Re-introduce failure processors toggle * Fix typo * Add Handler types for processor editor item * Fix staleness bug for type, refactor classname and fix duplicate bug not immutably copying values * Experimenting with padels (revert this to undo if no further changes have been made) * Add description and unique ids * Share links via component-wide context rather than props * Virtualized list and back to outline dropzones * Refactor id getter to a service and make it an incrementing number * Temporary fix for double rendering issue. This means the pipeline processors component is not controllable but fixes the double rendering even when processors have not changed. This is a problem coming from the ingest pipelines plugin form system outside of this component * add todo comment * remove euicode element * properly handle duplicate flow * attempt to fix i18n * split private_tree into it's own component and add comments * refactor 🚜. rename Tree to ProcessorsTree and move things around * do not delete the top level arrays for processors and onfailures * fix typescript error * Move duplicate, addOnFailure and delete actions into ctx menu * remove unused import * add support for pressing esc key to cancel move * Add outside click listener * always prompt before deleting a processor * refactor remove distinction between adding top level and on fail * add processor button to tree * Hide the add on failure context menu item for processors with failure handlers * Reinstated x-tree moving and highlight and disable for buttons on move and on edit * removing ids step 1: remove idGenerator * remove ids step 2: added inline text input Also refactored a lot of the tree actions away. Now using context and the processors editor tree item component for dispatching actions. The tree item has access to the dispatch and to the selector which makes it a well positioned component. Also saves on some props drilling. * Slight improvement to styling of text input (border) * Re-implement missing failure toggle test * address type todo * Address many type issues and fix yarn.lock * re-enable create pipeline functional test * prevent multiple flyouts from opening * change flyout title when editing an on-failure processor * absolutely position the failure handlers label when we render a dropzone on the label, then we move the label up without affecting overall component height * fix description behaviour not removing tag if empty * some minor clean up * add onflyoutopen cb to tests * refactor processors editor item to multiple files also refactored i18n into it's own file. would be good to come up with a pttaern for doing this more broadly. * fix add on-failure handler in context menu after refactor * tag -> new description field Co-authored-by: Alison Goryachev Co-authored-by: Elastic Machine Co-authored-by: Alison Goryachev --- x-pack/package.json | 1 + .../ingest_pipelines_create.test.tsx | 15 - .../plugins/ingest_pipelines/common/types.ts | 14 +- .../pipeline_form/pipeline_form.tsx | 50 ++- .../pipeline_form/pipeline_form_fields.tsx | 167 ++------ .../pipeline_request_flyout_provider.tsx | 22 +- .../pipeline_test_flyout_provider.tsx | 14 +- .../components/pipeline_form/types.ts | 9 + .../pipeline_processors_editor.helpers.ts | 36 ++ .../pipeline_processors_editor.test.tsx | 67 ++++ .../components/add_processor_button.tsx | 32 ++ .../components/index.ts | 19 + .../on_failure_processors_title.tsx | 52 +++ .../context_menu.tsx | 84 ++++ .../pipeline_processors_editor_item/index.ts | 7 + .../inline_text_input.tsx | 75 ++++ .../messages.ts | 58 +++ .../pipeline_processors_editor_item.scss | 17 + .../pipeline_processors_editor_item.tsx | 145 +++++++ .../components/processor_remove_modal.tsx | 54 +++ .../processor_settings_form/index.ts | 10 + .../map_processor_type_to_form.tsx | 56 +++ .../processor_settings_form.container.tsx | 56 +++ .../processor_settings_form.tsx | 73 ++++ .../common_fields/common_processor_fields.tsx | 55 +++ .../processors/common_fields/index.ts | 9 + .../common_fields/processor_type_field.tsx | 67 ++++ .../processors/custom.tsx | 90 +++++ .../processors/gsub.tsx | 98 +++++ .../processors/set.tsx | 74 ++++ .../processors_title_and_test_button.tsx | 73 ++++ .../components/drop_zone_button.tsx | 42 ++ .../processors_tree/components/index.ts | 11 + .../components/private_tree.tsx | 210 ++++++++++ .../processors_tree/components/tree_node.tsx | 115 ++++++ .../components/processors_tree/index.ts | 7 + .../processors_tree/processors_tree.scss | 74 ++++ .../processors_tree/processors_tree.tsx | 110 +++++ .../components/processors_tree/utils.ts | 41 ++ .../components/settings_form_flyout.tsx | 67 ++++ .../pipeline_processors_editor/constants.ts | 10 + .../pipeline_processors_editor/context.tsx | 55 +++ .../pipeline_processors_editor/deserialize.ts | 56 +++ .../pipeline_processors_editor/index.ts | 11 + .../pipeline_processors_editor.container.tsx | 76 ++++ .../pipeline_processors_editor.scss | 3 + .../pipeline_processors_editor.tsx | 239 +++++++++++ .../processors_reducer/index.ts | 15 + .../processors_reducer.test.ts | 376 ++++++++++++++++++ .../processors_reducer/processors_reducer.ts | 136 +++++++ .../processors_reducer/utils.ts | 100 +++++ .../pipeline_processors_editor/serialize.ts | 49 +++ .../pipeline_processors_editor/types.ts | 51 +++ .../pipeline_processors_editor/utils.test.ts | 36 ++ .../pipeline_processors_editor/utils.ts | 101 +++++ .../ingest_pipelines/public/shared_imports.ts | 9 + .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - .../apps/ingest_pipelines/ingest_pipelines.ts | 16 - 59 files changed, 3438 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts diff --git a/x-pack/package.json b/x-pack/package.json index 3b8991cb9fe39..172a6bc30125b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -331,6 +331,7 @@ "react-syntax-highlighter": "^5.7.0", "react-tiny-virtual-list": "^2.2.0", "react-use": "^13.27.0", + "react-virtualized": "^9.21.2", "react-vis": "^1.8.1", "react-visibility-sensor": "^5.1.1", "recompose": "^0.26.0", diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 6acb6369e2e90..4318062491df8 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -68,21 +68,6 @@ describe('', () => { expect(exists('versionField')).toBe(true); }); - test('should toggle the on-failure processors editor', async () => { - const { actions, component, exists } = testBed; - - // On-failure editor should be hidden by default - expect(exists('onFailureEditor')).toBe(false); - - await act(async () => { - actions.toggleOnFailureSwitch(); - await nextTick(); - component.update(); - }); - - expect(exists('onFailureEditor')).toBe(true); - }); - test('should show the request flyout', async () => { const { actions, component, find, exists } = testBed; diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 8d77359a7c3c5..ad6d1cc8aa928 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Processor { - [key: string]: { - [key: string]: unknown; - }; +export interface ESProcessorConfig { + on_failure?: Processor[]; + ignore_failure?: boolean; + if?: string; + tag?: string; + [key: string]: any; +} + +export interface Processor { + [typeName: string]: ESProcessorConfig; } export interface Pipeline { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 73bc1cfaa2cf5..ec065a74abca0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; @@ -16,6 +16,11 @@ import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; +import { + OnUpdateHandlerArg, + OnUpdateHandler, + SerializeResult, +} from '../pipeline_processors_editor'; export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; @@ -30,8 +35,8 @@ export const PipelineForm: React.FunctionComponent = ({ defaultValue = { name: '', description: '', - processors: '', - on_failure: '', + processors: [], + on_failure: [], version: '', }, onSave, @@ -44,10 +49,25 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); + const processorStateRef = useRef(); + const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { - if (isValid) { - onSave(formData as Pipeline); + let override: SerializeResult | undefined; + + if (!isValid) { + return; + } + + if (processorStateRef.current) { + const processorsState = processorStateRef.current; + if (await processorsState.validate()) { + override = processorsState.getData(); + } else { + return; + } } + + onSave({ ...formData, ...(override || {}) } as Pipeline); }; const handleTestPipelineClick = () => { @@ -60,6 +80,10 @@ export const PipelineForm: React.FunctionComponent = ({ onSubmit: handleSave, }); + const onEditorFlyoutOpen = useCallback(() => { + setIsRequestVisible(false); + }, [setIsRequestVisible]); + const saveButtonLabel = isSaving ? ( = ({ /> ); + const onProcessorsChangeHandler = useCallback( + (arg) => (processorStateRef.current = arg), + [] + ); + return ( <>
= ({ {/* All form fields */} @@ -147,6 +179,9 @@ export const PipelineForm: React.FunctionComponent = ({ {/* ES request flyout */} {isRequestVisible ? ( + processorStateRef.current?.getData() || { processors: [], on_failure: [] } + } closeFlyout={() => setIsRequestVisible((prevIsRequestVisible) => !prevIsRequestVisible)} /> ) : null} @@ -154,6 +189,9 @@ export const PipelineForm: React.FunctionComponent = ({ {/* Test pipeline flyout */} {isTestingPipeline ? ( + processorStateRef.current?.getData() || { processors: [], on_failure: [] } + } closeFlyout={() => { setIsTestingPipeline((prevIsTestingPipeline) => !prevIsTestingPipeline); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 9fb5ab55a34ce..a0e7c8fd8bcd7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,22 +6,22 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { - getUseField, - getFormRow, - Field, - JsonEditorField, - useKibana, -} from '../../../shared_imports'; +import { Processor } from '../../../../common/types'; +import { FormDataProvider } from '../../../shared_imports'; +import { PipelineProcessorsEditor, OnUpdateHandler } from '../pipeline_processors_editor'; + +import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports'; interface Props { + initialProcessors: Processor[]; + initialOnFailureProcessors?: Processor[]; + onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; - hasOnFailure: boolean; isTestButtonDisabled: boolean; onTestPipelineClick: () => void; + onEditorFlyoutOpen: () => void; isEditing?: boolean; } @@ -29,16 +29,18 @@ const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); export const PipelineFormFields: React.FunctionComponent = ({ + initialProcessors, + initialOnFailureProcessors, + onProcessorsUpdate, isEditing, hasVersion, - hasOnFailure, isTestButtonDisabled, onTestPipelineClick, + onEditorFlyoutOpen, }) => { const { services } = useKibana(); const [isVersionVisible, setIsVersionVisible] = useState(hasVersion); - const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(hasOnFailure); return ( <> @@ -110,127 +112,32 @@ export const PipelineFormFields: React.FunctionComponent = ({ /> - {/* Processors field */} - - } - description={ - <> - - {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { - defaultMessage: 'Learn more', - })} - - ), - }} - /> - - + {/* Pipeline Processors Editor */} + + {({ processors, on_failure: onFailure }) => { + const processorProp = + typeof processors === 'string' && processors + ? JSON.parse(processors) + : initialProcessors ?? []; - - - - - } - > - - + const onFailureProp = + typeof onFailure === 'string' && onFailure + ? JSON.parse(onFailure) + : initialOnFailureProcessors ?? []; - {/* On-failure field */} - - } - description={ - <> - - {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { - defaultMessage: 'Learn more', - })} - - ), - }} - /> - - - } - checked={isOnFailureEditorVisible} - onChange={(e) => setIsOnFailureEditorVisible(e.target.checked)} - data-test-subj="onFailureToggle" + return ( + - - } - > - {isOnFailureEditorVisible ? ( - - ) : ( - // requires children or a field - // For now, we return an empty
if the editor is not visible -
- )} - + ); + }} + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx index 6dcedca6085af..dd2439433fc41 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -4,13 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, FunctionComponent } from 'react'; import { Pipeline } from '../../../../../common/types'; import { useFormContext } from '../../../../shared_imports'; + +import { ReadProcessorsFunction } from '../types'; + import { PipelineRequestFlyout } from './pipeline_request_flyout'; -export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { +interface Props { + closeFlyout: () => void; + readProcessors: ReadProcessorsFunction; +} + +export const PipelineRequestFlyoutProvider: FunctionComponent = ({ + closeFlyout, + readProcessors, +}) => { const form = useFormContext(); const [formData, setFormData] = useState({} as Pipeline); @@ -25,5 +36,10 @@ export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () return subscription.unsubscribe; }, [form]); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx index 351478394595a..7f91672d64df4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx @@ -8,11 +8,19 @@ import React, { useState, useEffect } from 'react'; import { Pipeline } from '../../../../../common/types'; import { useFormContext } from '../../../../shared_imports'; + +import { ReadProcessorsFunction } from '../types'; + import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; -type Props = Omit; +interface Props extends Omit { + readProcessors: ReadProcessorsFunction; +} -export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ closeFlyout }) => { +export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ + closeFlyout, + readProcessors, +}) => { const form = useFormContext(); const [formData, setFormData] = useState({} as Pipeline); const [isFormDataValid, setIsFormDataValid] = useState(false); @@ -31,7 +39,7 @@ export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ clo return ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts new file mode 100644 index 0000000000000..bd74f09546ff4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Pipeline } from '../../../../common/types'; + +export type ReadProcessorsFunction = () => Pick; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts new file mode 100644 index 0000000000000..acd61a9bbd01e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerTestBed, TestBed } from '../../../../../../../test_utils'; +import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container'; + +const testBedSetup = registerTestBed(PipelineProcessorsEditor, { + doMountAsync: false, +}); + +export interface SetupResult extends TestBed { + actions: { + toggleOnFailure: () => void; + }; +} + +export const setup = async (props: Props): Promise => { + const testBed = await testBedSetup(props); + const toggleOnFailure = () => { + const { find } = testBed; + find('pipelineEditorOnFailureToggle').simulate('click'); + }; + + return { + ...testBed, + actions: { toggleOnFailure }, + }; +}; + +type TestSubject = + | 'pipelineEditorDoneButton' + | 'pipelineEditorOnFailureToggle' + | 'pipelineEditorOnFailureTree'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx new file mode 100644 index 0000000000000..758d6f5e620ce --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setup } from './pipeline_processors_editor.helpers'; +import { Pipeline } from '../../../../../common/types'; + +const testProcessors: Pick = { + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], +}; + +describe('Pipeline Editor', () => { + it('provides the same data out it got in if nothing changes', async () => { + const onUpdate = jest.fn(); + + await setup({ + value: { + ...testProcessors, + }, + onFlyoutOpen: jest.fn(), + onUpdate, + isTestButtonDisabled: false, + onTestPipelineClick: jest.fn(), + learnMoreAboutProcessorsUrl: 'test', + learnMoreAboutOnFailureProcessorsUrl: 'test', + }); + + const { + calls: [[arg]], + } = onUpdate.mock; + + expect(arg.getData()).toEqual(testProcessors); + }); + + it('toggles the on-failure processors', async () => { + const { actions, exists } = await setup({ + value: { + ...testProcessors, + }, + onFlyoutOpen: jest.fn(), + onUpdate: jest.fn(), + isTestButtonDisabled: false, + onTestPipelineClick: jest.fn(), + learnMoreAboutProcessorsUrl: 'test', + learnMoreAboutOnFailureProcessorsUrl: 'test', + }); + + expect(exists('pipelineEditorOnFailureTree')).toBe(false); + actions.toggleOnFailure(); + expect(exists('pipelineEditorOnFailureTree')).toBe(true); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx new file mode 100644 index 0000000000000..5f9bf87ceca1e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { usePipelineProcessorsContext } from '../context'; + +export interface Props { + onClick: () => void; +} + +export const AddProcessorButton: FunctionComponent = ({ onClick }) => { + const { + state: { editor }, + } = usePipelineProcessorsContext(); + return ( + + {i18n.translate('xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', { + defaultMessage: 'Add a processor', + })} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts new file mode 100644 index 0000000000000..cb5d5a10e9f42 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SettingsFormFlyout, OnSubmitHandler } from './settings_form_flyout'; + +export { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from './processor_settings_form'; + +export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; + +export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item'; + +export { ProcessorRemoveModal } from './processor_remove_modal'; + +export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button'; + +export { OnFailureProcessorsTitle } from './on_failure_processors_title'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx new file mode 100644 index 0000000000000..1c8edac7cfd64 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { usePipelineProcessorsContext } from '../context'; + +export const OnFailureProcessorsTitle: FunctionComponent = () => { + const { links } = usePipelineProcessorsContext(); + return ( + + + +

+ +

+
+ + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx new file mode 100644 index 0000000000000..bc7d6fdcff357 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState } from 'react'; + +import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui'; + +import { editorItemMessages } from './messages'; + +interface Props { + disabled: boolean; + showAddOnFailure: boolean; + onDuplicate: () => void; + onDelete: () => void; + onAddOnFailure: () => void; +} + +export const ContextMenu: FunctionComponent = ({ + showAddOnFailure, + onDuplicate, + onAddOnFailure, + onDelete, + disabled, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const contextMenuItems = [ + { + setIsOpen(false); + onDuplicate(); + }} + > + {editorItemMessages.duplicateButtonLabel} + , + showAddOnFailure ? ( + { + setIsOpen(false); + onAddOnFailure(); + }} + > + {editorItemMessages.addOnFailureButtonLabel} + + ) : undefined, + { + setIsOpen(false); + onDelete(); + }} + > + {editorItemMessages.deleteButtonLabel} + , + ].filter(Boolean) as JSX.Element[]; + + return ( + setIsOpen(false)} + button={ + setIsOpen((v) => !v)} + iconType="boxesHorizontal" + aria-label={editorItemMessages.moreButtonAriaLabel} + /> + } + > + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts new file mode 100644 index 0000000000000..02bafdb326024 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineProcessorsEditorItem, Handlers } from './pipeline_processors_editor_item'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx new file mode 100644 index 0000000000000..e0b67bc907ca9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState, useEffect, useCallback } from 'react'; +import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui'; + +export interface Props { + placeholder: string; + ariaLabel: string; + onChange: (value: string) => void; + text?: string; +} + +export const InlineTextInput: FunctionComponent = ({ + placeholder, + text, + ariaLabel, + onChange, +}) => { + const [isShowingTextInput, setIsShowingTextInput] = useState(false); + const [textValue, setTextValue] = useState(text ?? ''); + + const content = isShowingTextInput ? ( + el?.focus()} + onChange={(event) => setTextValue(event.target.value)} + /> + ) : ( + + {text || {placeholder}} + + ); + + const submitChange = useCallback(() => { + setIsShowingTextInput(false); + onChange(textValue); + }, [setIsShowingTextInput, onChange, textValue]); + + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') { + setIsShowingTextInput(false); + } + if (event.keyCode === keyCodes.ENTER || event.code === 'Enter') { + submitChange(); + } + }; + if (isShowingTextInput) { + window.addEventListener('keyup', keyboardListener); + } + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [isShowingTextInput, submitChange, setIsShowingTextInput]); + + return ( +
setIsShowingTextInput(true)} + onBlur={submitChange} + > + {content} +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts new file mode 100644 index 0000000000000..67dbf2708d665 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const editorItemMessages = { + moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { + defaultMessage: 'Move this processor', + }), + editorButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel', + { + defaultMessage: 'Edit this processor', + } + ), + duplicateButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel', + { + defaultMessage: 'Duplicate this processor', + } + ), + addOnFailureButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.addOnFailureHandlerButtonLabel', + { + defaultMessage: 'Add on failure handler', + } + ), + cancelMoveButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel', + { + defaultMessage: 'Cancel moving this processor', + } + ), + deleteButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + moreButtonAriaLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreButtonAriaLabel', + { + defaultMessage: 'Show more actions for this processor', + } + ), + processorTypeLabel: ({ type }: { type: string }) => + i18n.translate('xpack.ingestPipelines.pipelineEditor.item.textInputAriaLabel', { + defaultMessage: 'Provide a description for this {type} processor', + values: { type }, + }), + descriptionPlaceholder: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.descriptionPlaceholder', + { defaultMessage: 'No description' } + ), +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss new file mode 100644 index 0000000000000..a17e644853847 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -0,0 +1,17 @@ +.pipelineProcessorsEditor__item { + &__textContainer { + padding: 4px; + border-radius: 2px; + + transition: border-color .3s; + border: 2px solid #FFF; + + &:hover { + border: 2px solid $euiColorLightShade; + } + } + &__textInput { + height: 21px; + min-width: 100px; + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx new file mode 100644 index 0000000000000..0e47b3ef7cf88 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, memo } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; + +import { ProcessorInternal, ProcessorSelector } from '../../types'; + +import { usePipelineProcessorsContext } from '../../context'; + +import './pipeline_processors_editor_item.scss'; + +import { InlineTextInput } from './inline_text_input'; +import { ContextMenu } from './context_menu'; +import { editorItemMessages } from './messages'; + +export interface Handlers { + onMove: () => void; + onCancelMove: () => void; +} + +export interface Props { + processor: ProcessorInternal; + selected: boolean; + handlers: Handlers; + selector: ProcessorSelector; + description?: string; +} + +export const PipelineProcessorsEditorItem: FunctionComponent = memo( + ({ processor, description, handlers: { onCancelMove, onMove }, selector, selected }) => { + const { + state: { editor, processorsDispatch }, + } = usePipelineProcessorsContext(); + + const disabled = editor.mode.id !== 'idle'; + const isDarkBold = + editor.mode.id !== 'editingProcessor' || processor.id === editor.mode.arg.processor.id; + + return ( + + + + + + {processor.type} + + + + { + let nextOptions: Record; + if (!nextDescription) { + const { description: __, ...restOptions } = processor.options; + nextOptions = restOptions; + } else { + nextOptions = { + ...processor.options, + description: nextDescription, + }; + } + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...processor, + options: nextOptions, + }, + selector, + }, + }); + }} + ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} + text={description} + placeholder={editorItemMessages.descriptionPlaceholder} + /> + + + { + editor.setMode({ + id: 'editingProcessor', + arg: { processor, selector }, + }); + }} + /> + + + {selected ? ( + + ) : ( + + + + )} + + + + + { + editor.setMode({ id: 'creatingProcessor', arg: { selector } }); + }} + onDelete={() => { + editor.setMode({ id: 'removingProcessor', arg: { selector } }); + }} + onDuplicate={() => { + processorsDispatch({ + type: 'duplicateProcessor', + payload: { + source: selector, + }, + }); + }} + /> + + + ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx new file mode 100644 index 0000000000000..c38e470b36699 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { ProcessorInternal, ProcessorSelector } from '../types'; + +interface Props { + processor: ProcessorInternal; + selector: ProcessorSelector; + onResult: (arg: { confirmed: boolean; selector: ProcessorSelector }) => void; +} + +export const ProcessorRemoveModal = ({ processor, onResult, selector }: Props) => { + return ( + + + } + onCancel={() => onResult({ confirmed: false, selector })} + onConfirm={() => onResult({ confirmed: true, selector })} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts new file mode 100644 index 0000000000000..60a1aa0a96fb1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + ProcessorSettingsForm, + ProcessorSettingsFromOnSubmitArg, +} from './processor_settings_form.container'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx new file mode 100644 index 0000000000000..e8164a0057d39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FunctionComponent } from 'react'; + +// import { SetProcessor } from './processors/set'; +// import { Gsub } from './processors/gsub'; + +const mapProcessorTypeToForm = { + append: undefined, // TODO: Implement + bytes: undefined, // TODO: Implement + circle: undefined, // TODO: Implement + convert: undefined, // TODO: Implement + csv: undefined, // TODO: Implement + date: undefined, // TODO: Implement + date_index_name: undefined, // TODO: Implement + dissect: undefined, // TODO: Implement + dot_expander: undefined, // TODO: Implement + drop: undefined, // TODO: Implement + enrich: undefined, // TODO: Implement + fail: undefined, // TODO: Implement + foreach: undefined, // TODO: Implement + geoip: undefined, // TODO: Implement + grok: undefined, // TODO: Implement + html_strip: undefined, // TODO: Implement + inference: undefined, // TODO: Implement + join: undefined, // TODO: Implement + json: undefined, // TODO: Implement + kv: undefined, // TODO: Implement + lowercase: undefined, // TODO: Implement + pipeline: undefined, // TODO: Implement + remove: undefined, // TODO: Implement + rename: undefined, // TODO: Implement + script: undefined, // TODO: Implement + set_security_user: undefined, // TODO: Implement + split: undefined, // TODO: Implement + sort: undefined, // TODO: Implement + trim: undefined, // TODO: Implement + uppercase: undefined, // TODO: Implement + urldecode: undefined, // TODO: Implement + user_agent: undefined, // TODO: Implement + + gsub: undefined, + set: undefined, +}; + +export const types = Object.keys(mapProcessorTypeToForm); + +export type ProcessorType = keyof typeof mapProcessorTypeToForm; + +export const getProcessorForm = (type: ProcessorType | string): FunctionComponent | undefined => { + return mapProcessorTypeToForm[type as ProcessorType]; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx new file mode 100644 index 0000000000000..29b52ef84600a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useCallback, useEffect } from 'react'; + +import { useForm, OnFormUpdateArg, FormData } from '../../../../../shared_imports'; +import { ProcessorInternal } from '../../types'; + +import { ProcessorSettingsForm as ViewComponent } from './processor_settings_form'; + +export type ProcessorSettingsFromOnSubmitArg = Omit; + +interface Props { + onFormUpdate: (form: OnFormUpdateArg) => void; + onSubmit: (processor: ProcessorSettingsFromOnSubmitArg) => void; + processor?: ProcessorInternal; +} + +export const ProcessorSettingsForm: FunctionComponent = ({ + processor, + onFormUpdate, + onSubmit, +}) => { + const handleSubmit = useCallback( + async (data: FormData, isValid: boolean) => { + if (isValid) { + const { type, customOptions, ...options } = data; + onSubmit({ + type, + options: customOptions ? customOptions : options, + }); + } + }, + [onSubmit] + ); + + const { form } = useForm({ + defaultValue: processor?.options, + onSubmit: handleSubmit, + }); + + useEffect(() => { + const subscription = form.subscribe(onFormUpdate); + return subscription.unsubscribe; + + // TODO: Address this issue + // For some reason adding `form` object to the dependencies array here is causing an + // infinite update loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onFormUpdate]); + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx new file mode 100644 index 0000000000000..49bde2129aab6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent, memo } from 'react'; +import { EuiButton, EuiHorizontalRule } from '@elastic/eui'; + +import { Form, useForm, FormDataProvider } from '../../../../../shared_imports'; + +import { ProcessorInternal } from '../../types'; + +import { getProcessorForm } from './map_processor_type_to_form'; +import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields'; +import { Custom } from './processors/custom'; + +export interface Props { + processor?: ProcessorInternal; + form: ReturnType['form']; +} + +export const ProcessorSettingsForm: FunctionComponent = memo( + ({ processor, form }) => { + return ( + + + + + + + {(arg: any) => { + const { type } = arg; + let formContent: React.ReactNode | undefined; + + if (type?.length) { + const ProcessorFormFields = getProcessorForm(type as any); + + if (ProcessorFormFields) { + formContent = ( + <> + + + + ); + } else { + formContent = ; + } + + return ( + <> + {formContent} + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel', + { defaultMessage: 'Submit' } + )} + + + ); + } + + // If the user has not yet defined a type, we do not show any settings fields + return null; + }} + + + ); + }, + (previous, current) => { + return previous.processor === current.processor; + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx new file mode 100644 index 0000000000000..4802653f9e680 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FieldConfig, + UseField, + FIELD_TYPES, + Field, + ToggleField, +} from '../../../../../../../shared_imports'; + +const ignoreFailureConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', + { + defaultMessage: 'Ignore failure', + } + ), + type: FIELD_TYPES.TOGGLE, +}; + +const ifConfig: FieldConfig = { + defaultValue: undefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { + defaultMessage: 'Condition (optional)', + }), + type: FIELD_TYPES.TEXT, +}; + +const tagConfig: FieldConfig = { + defaultValue: undefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { + defaultMessage: 'Tag (optional)', + }), + type: FIELD_TYPES.TEXT, +}; + +export const CommonProcessorFields: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts new file mode 100644 index 0000000000000..f3fa0e028faaa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ProcessorTypeField } from './processor_type_field'; + +export { CommonProcessorFields } from './common_processor_fields'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx new file mode 100644 index 0000000000000..6c86fc16bcdd0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { + FIELD_TYPES, + FieldConfig, + UseField, + fieldValidators, + ComboBoxField, +} from '../../../../../../../shared_imports'; +import { types } from '../../map_processor_type_to_form'; + +interface Props { + initialType?: string; +} + +const { emptyField } = fieldValidators; + +const typeConfig: FieldConfig = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', { + defaultMessage: 'Processor', + }), + deserializer: (value: string | undefined) => { + if (value) { + return [value]; + } + return []; + }, + serializer: (value: string[]) => { + return value[0]; + }, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError', { + defaultMessage: 'A type is required.', + }) + ), + }, + ], +}; + +export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { + return ( + ({ label: type, value: type })), + noSuggestions: false, + singleSelection: { + asPlainText: true, + }, + }, + }} + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx new file mode 100644 index 0000000000000..61fc31a7b472a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FieldConfig, + FIELD_TYPES, + fieldValidators, + UseField, + JsonEditorField, +} from '../../../../../../shared_imports'; + +const { emptyField, isJsonField } = fieldValidators; + +const customConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel', { + defaultMessage: 'Configuration options', + }), + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, + deserializer: (value: any) => { + if (value === '') { + return '{\n\n}'; + } + return JSON.stringify(value, null, 2); + }, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError', + { + defaultMessage: 'Configuration options are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.invalidJsonError', { + defaultMessage: 'The input is not valid.', + }) + ), + }, + ], +}; + +interface Props { + defaultOptions?: any; +} + +/** + * This is a catch-all component to support settings for custom processors + * or existing processors not yet supported by the UI. + * + * We store the settings in a field called "customOptions" + **/ +export const Custom: FunctionComponent = ({ defaultOptions }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx new file mode 100644 index 0000000000000..77f85e61eff6b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FieldConfig, + FIELD_TYPES, + fieldValidators, + ToggleField, + UseField, + Field, +} from '../../../../../../shared_imports'; + +const { emptyField } = fieldValidators; + +const fieldConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], +}; + +const patternConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel', { + defaultMessage: 'Pattern', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError', { + defaultMessage: 'A pattern value is required.', + }) + ), + }, + ], +}; + +const replacementConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel', { + defaultMessage: 'Replacement', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError', { + defaultMessage: 'A replacement value is required.', + }) + ), + }, + ], +}; + +const targetConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), +}; + +const ignoreMissingConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.ignoreMissingFieldLabel', { + defaultMessage: 'Ignore missing', + }), + type: FIELD_TYPES.TOGGLE, +}; + +export const Gsub: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx new file mode 100644 index 0000000000000..1ba6a14d0448d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FieldConfig, + FIELD_TYPES, + fieldValidators, + ToggleField, + UseField, + Field, +} from '../../../../../../shared_imports'; + +const { emptyField } = fieldValidators; + +const fieldConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], +}; + +const valueConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { + defaultMessage: 'A value to set is required.', + }) + ), + }, + ], +}; + +const overrideConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { + defaultMessage: 'Override', + }), + type: FIELD_TYPES.TOGGLE, +}; + +/** + * Disambiguate name from the Set data structure + */ +export const SetProcessor: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx new file mode 100644 index 0000000000000..bc646c9eefa55 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { usePipelineProcessorsContext } from '../context'; + +export interface Props { + onTestPipelineClick: () => void; + isTestButtonDisabled: boolean; +} + +export const ProcessorsTitleAndTestButton: FunctionComponent = ({ + onTestPipelineClick, + isTestButtonDisabled, +}) => { + const { links } = usePipelineProcessorsContext(); + return ( + + + +

+ {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { + defaultMessage: 'Processors', + })} +

+
+ + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + +
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx new file mode 100644 index 0000000000000..a47886292cf32 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; +import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; + +export interface Props { + isDisabled: boolean; + onClick: (event: React.MouseEvent) => void; +} + +const MOVE_HERE_LABEL = i18n.translate('xpack.ingestPipelines.pipelineEditor.moveTargetLabel', { + defaultMessage: 'Move here', +}); + +export const DropZoneButton: FunctionComponent = ({ onClick, isDisabled }) => { + const containerClasses = classNames({ + 'pipelineProcessorsEditor__tree__dropZoneContainer--active': !isDisabled, + }); + const buttonClasses = classNames({ + 'pipelineProcessorsEditor__tree__dropZoneButton--active': !isDisabled, + }); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts new file mode 100644 index 0000000000000..e9548624d2cef --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DropZoneButton } from './drop_zone_button'; + +export { PrivateTree } from './private_tree'; + +export { TreeNode } from './tree_node'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx new file mode 100644 index 0000000000000..bdc6b2eb44e2d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, MutableRefObject, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AutoSizer, List, WindowScroller } from 'react-virtualized'; + +import { DropSpecialLocations } from '../../../constants'; +import { ProcessorInternal, ProcessorSelector } from '../../../types'; +import { isChildPath } from '../../../processors_reducer'; + +import { DropZoneButton } from '.'; +import { TreeNode } from '.'; +import { calculateItemHeight } from '../utils'; +import { OnActionHandler, ProcessorInfo } from '../processors_tree'; + +export interface PrivateProps { + processors: ProcessorInternal[]; + selector: ProcessorSelector; + onAction: OnActionHandler; + level: number; + movingProcessor?: ProcessorInfo; + // Only passed into the top level list + windowScrollerRef?: MutableRefObject; + listRef?: MutableRefObject; +} + +const isDropZoneAboveDisabled = (processor: ProcessorInfo, selectedProcessor: ProcessorInfo) => { + return Boolean( + // Is the selected node first in a list? + (!selectedProcessor.aboveId && selectedProcessor.id === processor.id) || + isChildPath(selectedProcessor.selector, processor.selector) + ); +}; + +const isDropZoneBelowDisabled = (processor: ProcessorInfo, selectedProcessor: ProcessorInfo) => { + return ( + processor.id === selectedProcessor.id || + processor.belowId === selectedProcessor.id || + isChildPath(selectedProcessor.selector, processor.selector) + ); +}; + +/** + * Recursively rendering tree component for ingest pipeline processors. + * + * Note: this tree should start at level 1. It is the only level at + * which we render the optimised virtual component. This gives a + * massive performance boost to this component which can get very tall. + * + * The first level list also contains the outside click listener which + * enables users to click outside of the tree and cancel moving a + * processor. + */ +export const PrivateTree: FunctionComponent = ({ + processors, + selector, + movingProcessor, + onAction, + level, + windowScrollerRef, + listRef, +}) => { + const renderRow = ({ + idx, + info, + processor, + }: { + idx: number; + info: ProcessorInfo; + processor: ProcessorInternal; + }) => { + return ( + <> + {idx === 0 ? ( + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: selector.concat(DropSpecialLocations.top), + source: movingProcessor!.selector, + }, + }); + }} + isDisabled={Boolean( + !movingProcessor || isDropZoneAboveDisabled(info, movingProcessor!) + )} + /> + ) : undefined} + + + + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: selector.concat(String(idx + 1)), + source: movingProcessor!.selector, + }, + }); + }} + /> + + ); + }; + + useEffect(() => { + if (windowScrollerRef && windowScrollerRef.current) { + windowScrollerRef.current.updatePosition(); + } + if (listRef && listRef.current) { + listRef.current.recomputeRowHeights(); + } + }, [processors, listRef, windowScrollerRef, movingProcessor]); + + // A list optimized to handle very many items. + const renderVirtualList = () => { + return ( + + {({ height, registerChild, isScrolling, onChildScroll, scrollTop }: any) => { + return ( + + + {({ width }) => { + return ( +
+ { + const processor = processors[index]; + return calculateItemHeight({ + processor, + isFirstInArray: index === 0, + }); + }} + rowRenderer={({ index: idx, style }) => { + const processor = processors[idx]; + const above = processors[idx - 1]; + const below = processors[idx + 1]; + const info: ProcessorInfo = { + id: processor.id, + selector: selector.concat(String(idx)), + aboveId: above?.id, + belowId: below?.id, + }; + + return ( +
+ {renderRow({ processor, info, idx })} +
+ ); + }} + processors={processors} + /> +
+ ); + }} +
+
+ ); + }} +
+ ); + }; + + if (level === 1) { + // Only render the optimised list for the top level list because that is the list + // that will almost certainly be the tallest + return renderVirtualList(); + } + + return ( + + {processors.map((processor, idx) => { + const above = processors[idx - 1]; + const below = processors[idx + 1]; + const info: ProcessorInfo = { + id: processor.id, + selector: selector.concat(String(idx)), + aboveId: above?.id, + belowId: below?.id, + }; + + return
{renderRow({ processor, idx, info })}
; + })} +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx new file mode 100644 index 0000000000000..ebe4ca4962b4c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useMemo } from 'react'; +import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiPanel, EuiText } from '@elastic/eui'; + +import { ProcessorInternal } from '../../../types'; + +import { ProcessorInfo, OnActionHandler } from '../processors_tree'; + +import { PipelineProcessorsEditorItem, Handlers } from '../../pipeline_processors_editor_item'; +import { AddProcessorButton } from '../../add_processor_button'; + +import { PrivateTree } from './private_tree'; + +export interface Props { + processor: ProcessorInternal; + processorInfo: ProcessorInfo; + onAction: OnActionHandler; + level: number; + movingProcessor?: ProcessorInfo; +} + +const INDENTATION_PX = 34; + +export const TreeNode: FunctionComponent = ({ + processor, + processorInfo, + onAction, + movingProcessor, + level, +}) => { + const stringSelector = processorInfo.selector.join('.'); + const handlers = useMemo((): Handlers => { + return { + onMove: () => { + onAction({ type: 'selectToMove', payload: { info: processorInfo } }); + }, + onCancelMove: () => { + onAction({ type: 'cancelMove' }); + }, + }; + }, [onAction, stringSelector, processor]); // eslint-disable-line react-hooks/exhaustive-deps + + const selected = movingProcessor?.id === processor.id; + + const panelClasses = classNames({ + 'pipelineProcessorsEditor__tree__item--selected': selected, + }); + + const renderOnFailureHandlersTree = () => { + if (!processor.onFailure?.length) { + return; + } + + const onFailureHandlerLabelClasses = classNames({ + 'pipelineProcessorsEditor__tree__onFailureHandlerLabel--withDropZone': + movingProcessor != null && + movingProcessor.id !== processor.onFailure[0].id && + movingProcessor.id !== processor.id, + }); + + return ( +
+
+ + {i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', { + defaultMessage: 'Failure handlers', + })} + +
+ + + onAction({ + type: 'addProcessor', + payload: { target: processorInfo.selector.concat('onFailure') }, + }) + } + /> +
+ ); + }; + + return ( + + + {renderOnFailureHandlersTree()} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts new file mode 100644 index 0000000000000..5a09794fd4bee --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ProcessorsTree, OnActionHandler, ProcessorInfo } from './processors_tree'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss new file mode 100644 index 0000000000000..ad9058cea5e18 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss @@ -0,0 +1,74 @@ +@import '@elastic/eui/src/global_styling/variables/size'; + +.pipelineProcessorsEditor__tree { + + &__container { + background-color: $euiColorLightestShade; + padding: $euiSizeS; + } + + &__dropZoneContainer { + margin: 2px; + visibility: hidden; + border: 2px dashed $euiColorLightShade; + height: 12px; + border-radius: 2px; + + transition: border .5s; + + &--active { + &:hover { + border: 2px dashed $euiColorPrimary; + } + visibility: visible; + } + } + + &__dropZoneButton { + height: 8px; + opacity: 0; + text-decoration: none !important; + + &--active { + &:hover { + transform: none !important; + } + } + + &:disabled { + cursor: default !important; + & > * { + cursor: default !important; + } + } + } + + &__onFailureHandlerLabelContainer { + position: relative; + height: 14px; + } + &__onFailureHandlerLabel { + position: absolute; + bottom: -16px; + &--withDropZone { + bottom: -4px; + } + } + + + &__onFailureHandlerContainer { + margin-top: $euiSizeS; + margin-bottom: $euiSizeS; + & > * { + overflow: visible; + } + } + + &__item { + transition: border-color 1s; + min-height: 50px; + &--selected { + border: 1px solid $euiColorPrimary; + } + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx new file mode 100644 index 0000000000000..d0661913515b2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent, memo, useRef, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, keyCodes } from '@elastic/eui'; +import { List, WindowScroller } from 'react-virtualized'; + +import { ProcessorInternal, ProcessorSelector } from '../../types'; + +import './processors_tree.scss'; +import { AddProcessorButton } from '../add_processor_button'; +import { PrivateTree } from './components'; + +export interface ProcessorInfo { + id: string; + selector: ProcessorSelector; + aboveId?: string; + belowId?: string; +} + +export type Action = + | { type: 'move'; payload: { source: ProcessorSelector; destination: ProcessorSelector } } + | { type: 'selectToMove'; payload: { info: ProcessorInfo } } + | { type: 'cancelMove' } + | { type: 'addProcessor'; payload: { target: ProcessorSelector } }; + +export type OnActionHandler = (action: Action) => void; + +export interface Props { + processors: ProcessorInternal[]; + baseSelector: ProcessorSelector; + onAction: OnActionHandler; + movingProcessor?: ProcessorInfo; + 'data-test-subj'?: string; +} + +/** + * This component is the public interface to our optimised tree rendering private components and + * also contains top-level state concerns for an instance of the component + */ +export const ProcessorsTree: FunctionComponent = memo((props) => { + const { processors, baseSelector, onAction, movingProcessor } = props; + // These refs are created here so they can be shared with all + // recursively rendered trees. Their values should come from react-virtualized + // List component and WindowScroller component. + const windowScrollerRef = useRef(null); + const listRef = useRef(null); + + useEffect(() => { + const cancelMoveKbListener = (event: KeyboardEvent) => { + // x-browser support per https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') { + onAction({ type: 'cancelMove' }); + } + }; + const cancelMoveClickListener = (ev: any) => { + onAction({ type: 'cancelMove' }); + }; + // Give the browser a chance to flush any click events including the click + // event that triggered any state transition into selecting a processor to move + setTimeout(() => { + if (movingProcessor) { + window.addEventListener('keyup', cancelMoveKbListener); + window.addEventListener('click', cancelMoveClickListener); + } else { + window.removeEventListener('keyup', cancelMoveKbListener); + window.removeEventListener('click', cancelMoveClickListener); + } + }); + return () => { + window.removeEventListener('keyup', cancelMoveKbListener); + window.removeEventListener('click', cancelMoveClickListener); + }; + }, [movingProcessor, onAction]); + + return ( + + + + + + + + { + onAction({ type: 'addProcessor', payload: { target: baseSelector } }); + }} + /> + + + + + ); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts new file mode 100644 index 0000000000000..457e335602b9b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessorInternal } from '../../types'; + +// These values are tied to the style and heights following components: +// Do not change these numbers without testing the component for visual +// regressions! +// - ./components/tree_node.tsx +// - ./components/drop_zone_button.tsx +// - ./components/pipeline_processors_editor_item.tsx +const itemHeightsPx = { + WITHOUT_NESTED_ITEMS: 67, + WITH_NESTED_ITEMS: 137, + TOP_PADDING: 16, +}; + +export const calculateItemHeight = ({ + processor, + isFirstInArray, +}: { + processor: ProcessorInternal; + isFirstInArray: boolean; +}): number => { + const padding = isFirstInArray ? itemHeightsPx.TOP_PADDING : 0; + + if (!processor.onFailure?.length) { + return padding + itemHeightsPx.WITHOUT_NESTED_ITEMS; + } + + return ( + padding + + itemHeightsPx.WITH_NESTED_ITEMS + + processor.onFailure.reduce((acc, p, idx) => { + return acc + calculateItemHeight({ processor: p, isFirstInArray: idx === 0 }); + }, 0) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx new file mode 100644 index 0000000000000..94d5f0eda6454 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +import React, { FunctionComponent, memo, useEffect } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { OnFormUpdateArg } from '../../../../shared_imports'; + +import { ProcessorInternal } from '../types'; + +import { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from '.'; + +export type OnSubmitHandler = (processor: ProcessorSettingsFromOnSubmitArg) => void; + +export interface Props { + processor: ProcessorInternal | undefined; + onFormUpdate: (form: OnFormUpdateArg) => void; + onSubmit: OnSubmitHandler; + isOnFailureProcessor: boolean; + onOpen: () => void; + onClose: () => void; +} + +export const SettingsFormFlyout: FunctionComponent = memo( + ({ onClose, processor, onSubmit, onFormUpdate, onOpen, isOnFailureProcessor }) => { + useEffect( + () => { + onOpen(); + }, + [] /* eslint-disable-line react-hooks/exhaustive-deps */ + ); + const flyoutTitleContent = isOnFailureProcessor ? ( + + ) : ( + + ); + + return ( + + + +

{flyoutTitleContent}

+
+
+ + + +
+ ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts new file mode 100644 index 0000000000000..46e3d1c803fd5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DropSpecialLocations { + top = 'TOP', + bottom = 'BOTTOM', +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx new file mode 100644 index 0000000000000..150a52f1a5fe0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, Dispatch, FunctionComponent, useContext, useState } from 'react'; +import { EditorMode } from './types'; +import { ProcessorsDispatch } from './processors_reducer'; + +interface Links { + learnMoreAboutProcessorsUrl: string; + learnMoreAboutOnFailureProcessorsUrl: string; +} + +const PipelineProcessorsContext = createContext<{ + links: Links; + state: { + processorsDispatch: ProcessorsDispatch; + editor: { + mode: EditorMode; + setMode: Dispatch; + }; + }; +}>({} as any); + +interface Props { + links: Links; + processorsDispatch: ProcessorsDispatch; +} + +export const PipelineProcessorsContextProvider: FunctionComponent = ({ + links, + children, + processorsDispatch, +}) => { + const [mode, setMode] = useState({ id: 'idle' }); + return ( + + {children} + + ); +}; + +export const usePipelineProcessorsContext = () => { + const ctx = useContext(PipelineProcessorsContext); + if (!ctx) { + throw new Error( + 'usePipelineProcessorsContext can only be used inside of PipelineProcessorsContextProvider' + ); + } + return ctx; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts new file mode 100644 index 0000000000000..fa1d041bdaba3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { Processor } from '../../../../common/types'; +import { ProcessorInternal } from './types'; + +export interface DeserializeArgs { + processors: Processor[]; + onFailure?: Processor[]; +} + +export interface DeserializeResult { + processors: ProcessorInternal[]; + onFailure?: ProcessorInternal[]; +} + +const getProcessorType = (processor: Processor): string => { + /** + * See the definition of {@link ProcessorInternal} for why this works to extract the + * processor type. + */ + return Object.keys(processor)[0]!; +}; + +const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { + const type = getProcessorType(processor); + const { on_failure: originalOnFailure, ...options } = processor[type]; + const onFailure = originalOnFailure?.length + ? convertProcessors(originalOnFailure) + : (originalOnFailure as ProcessorInternal[] | undefined); + return { + id: uuid.v4(), + type, + onFailure, + options, + }; +}; + +const convertProcessors = (processors: Processor[]) => { + const convertedProcessors = []; + + for (const processor of processors) { + convertedProcessors.push(convertToPipelineInternalProcessor(processor)); + } + return convertedProcessors; +}; + +export const deserialize = ({ processors, onFailure }: DeserializeArgs): DeserializeResult => { + return { + processors: convertProcessors(processors), + onFailure: onFailure ? convertProcessors(onFailure) : undefined, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts new file mode 100644 index 0000000000000..58d6e492b85e5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container'; + +export { OnUpdateHandlerArg } from './types'; + +export { SerializeResult } from './serialize'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx new file mode 100644 index 0000000000000..057f8638700a4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useMemo } from 'react'; + +import { Processor } from '../../../../common/types'; + +import { deserialize } from './deserialize'; + +import { useProcessorsState } from './processors_reducer'; + +import { PipelineProcessorsContextProvider } from './context'; + +import { OnUpdateHandlerArg } from './types'; + +import { PipelineProcessorsEditor as PipelineProcessorsEditorUI } from './pipeline_processors_editor'; + +export interface Props { + value: { + processors: Processor[]; + onFailure?: Processor[]; + }; + onUpdate: (arg: OnUpdateHandlerArg) => void; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + learnMoreAboutProcessorsUrl: string; + learnMoreAboutOnFailureProcessorsUrl: string; + /** + * Give users a way to react to this component opening a flyout + */ + onFlyoutOpen: () => void; +} + +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + +export const PipelineProcessorsEditor: FunctionComponent = ({ + value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, + onFlyoutOpen, + onUpdate, + isTestButtonDisabled, + learnMoreAboutOnFailureProcessorsUrl, + learnMoreAboutProcessorsUrl, + onTestPipelineClick, +}) => { + const deserializedResult = useMemo( + () => + deserialize({ + processors: originalProcessors, + onFailure: originalOnFailureProcessors, + }), + // TODO: Re-add the dependency on the props and make the state set-able + // when new props come in so that this component will be controllable + [] // eslint-disable-line react-hooks/exhaustive-deps + ); + const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + const { processors, onFailure } = processorsState; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss new file mode 100644 index 0000000000000..ee7421d7dbfa8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss @@ -0,0 +1,3 @@ +.pipelineProcessorsEditor { + margin-bottom: $euiSize; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..24b9598a74d47 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent, useCallback, memo, useState, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch } from '@elastic/eui'; + +import './pipeline_processors_editor.scss'; + +import { + ProcessorsTitleAndTestButton, + OnFailureProcessorsTitle, + ProcessorsTree, + SettingsFormFlyout, + ProcessorRemoveModal, + OnActionHandler, + OnSubmitHandler, +} from './components'; + +import { + ProcessorInternal, + ProcessorSelector, + OnUpdateHandlerArg, + FormValidityState, + OnFormUpdateArg, +} from './types'; + +import { serialize } from './serialize'; +import { getValue } from './utils'; +import { usePipelineProcessorsContext } from './context'; + +export interface Props { + processors: ProcessorInternal[]; + onFailureProcessors: ProcessorInternal[]; + onUpdate: (arg: OnUpdateHandlerArg) => void; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + onFlyoutOpen: () => void; +} + +const PROCESSOR_STATE_SCOPE: ProcessorSelector = ['processors']; +const ON_FAILURE_STATE_SCOPE: ProcessorSelector = ['onFailure']; + +export const PipelineProcessorsEditor: FunctionComponent = memo( + function PipelineProcessorsEditor({ + processors, + onFailureProcessors, + onTestPipelineClick, + isTestButtonDisabled, + onUpdate, + onFlyoutOpen, + }) { + const { + state: { editor, processorsDispatch }, + } = usePipelineProcessorsContext(); + + const { mode: editorMode, setMode: setEditorMode } = editor; + + const [formState, setFormState] = useState({ + validate: () => Promise.resolve(true), + }); + + const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( + ({ isValid, validate }) => { + setFormState({ + validate: async () => { + if (isValid === undefined) { + return validate(); + } + return isValid; + }, + }); + }, + [setFormState] + ); + + const [showGlobalOnFailure, setShowGlobalOnFailure] = useState( + Boolean(onFailureProcessors.length) + ); + + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && editorMode.id === 'idle'; + }, + getData: () => + serialize({ + onFailure: showGlobalOnFailure ? onFailureProcessors : undefined, + processors, + }), + }); + }, [processors, onFailureProcessors, onUpdate, formState, editorMode, showGlobalOnFailure]); + + const onSubmit = useCallback( + (processorTypeAndOptions) => { + switch (editorMode.id) { + case 'creatingProcessor': + processorsDispatch({ + type: 'addProcessor', + payload: { + processor: { ...processorTypeAndOptions }, + targetSelector: editorMode.arg.selector, + }, + }); + break; + case 'editingProcessor': + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...editorMode.arg.processor, + ...processorTypeAndOptions, + }, + selector: editorMode.arg.selector, + }, + }); + break; + default: + } + setEditorMode({ id: 'idle' }); + }, + [processorsDispatch, editorMode, setEditorMode] + ); + + const onCloseSettingsForm = useCallback(() => { + setEditorMode({ id: 'idle' }); + setFormState({ validate: () => Promise.resolve(true) }); + }, [setFormState, setEditorMode]); + + const onTreeAction = useCallback( + (action) => { + switch (action.type) { + case 'addProcessor': + setEditorMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); + break; + case 'move': + setEditorMode({ id: 'idle' }); + processorsDispatch({ + type: 'moveProcessor', + payload: action.payload, + }); + break; + case 'selectToMove': + setEditorMode({ id: 'movingProcessor', arg: action.payload.info }); + break; + case 'cancelMove': + setEditorMode({ id: 'idle' }); + break; + } + }, + [processorsDispatch, setEditorMode] + ); + + const movingProcessor = editorMode.id === 'movingProcessor' ? editorMode.arg : undefined; + + return ( +
+ + + + + + + + + + + + + + + + } + checked={showGlobalOnFailure} + onChange={(e) => setShowGlobalOnFailure(e.target.checked)} + data-test-subj="pipelineEditorOnFailureToggle" + /> + + {showGlobalOnFailure ? ( + + + + ) : undefined} + + {editorMode.id === 'editingProcessor' || editorMode.id === 'creatingProcessor' ? ( + 1} + processor={editorMode.id === 'editingProcessor' ? editorMode.arg.processor : undefined} + onOpen={onFlyoutOpen} + onFormUpdate={onFormUpdate} + onSubmit={onSubmit} + onClose={onCloseSettingsForm} + /> + ) : undefined} + {editorMode.id === 'removingProcessor' && ( + { + if (confirmed) { + processorsDispatch({ + type: 'removeProcessor', + payload: { selector }, + }); + } + setEditorMode({ id: 'idle' }); + }} + /> + )} +
+ ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts new file mode 100644 index 0000000000000..b43d94e19bf9f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + State, + reducer, + useProcessorsState, + ProcessorsDispatch, + Action, +} from './processors_reducer'; + +export { isChildPath } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts new file mode 100644 index 0000000000000..43072d65bac4e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { reducer, State } from './processors_reducer'; +import { DropSpecialLocations } from '../constants'; +import { PARENT_CHILD_NEST_ERROR } from './utils'; + +const initialState: State = { + processors: [], + onFailure: [], + isRoot: true, +}; + +describe('Processors reducer', () => { + it('reorders processors', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors'] }, + }); + + expect(s3.processors).toEqual([processor1, processor2, processor3]); + + // Move the second processor to the first + const s4 = reducer(s3, { + type: 'moveProcessor', + payload: { + source: ['processors', '1'], + destination: ['processors', '0'], + }, + }); + + expect(s4.processors).toEqual([processor2, processor1, processor3]); + }); + + it('moves and orders processors out of lists', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the first on failure processor of the second processors on failure processor + // to the second position of the root level. + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '1', 'onFailure', '0'], + destination: ['processors', '1'], + }, + }); + + expect(s5.processors).toEqual([ + processor1, + { ...processor3, onFailure: [processor4] }, + { ...processor2, onFailure: undefined }, + ]); + }); + + it('moves and orders processors into lists', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the first processor to the deepest most on-failure processor's failure processor + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '0'], + destination: ['processors', '1', 'onFailure', '0', 'onFailure', '0', 'onFailure', '0'], + }, + }); + + expect(s5.processors).toEqual([ + { + ...processor2, + onFailure: [{ ...processor3, onFailure: [{ ...processor4, onFailure: [processor1] }] }], + }, + ]); + }); + + it('handles sending processor to bottom correctly', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors'] }, + }); + + // Move the parent into a child list + const s4 = reducer(s3, { + type: 'moveProcessor', + payload: { + source: ['processors', '0'], + destination: ['processors', DropSpecialLocations.bottom], + }, + }); + + // Assert nothing changed + expect(s4.processors).toEqual([processor2, processor3, processor1]); + }); + + it('will not set the root "onFailure" to "undefined" if it is empty', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['onFailure'] }, + }); + + // Move the parent into a child list + const s3 = reducer(s2, { + type: 'moveProcessor', + payload: { + source: ['onFailure', '0'], + destination: ['processors', '1'], + }, + }); + + expect(s3).toEqual({ + processors: [processor1, processor2], + onFailure: [], + isRoot: true, + }); + }); + + it('places copies and places the copied processor below the original', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { + id: expect.any(String), + type: 'test4', + options: { field: 'field_name', value: 'field_value' }, + }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + const s5 = reducer(s4, { + type: 'duplicateProcessor', + payload: { source: ['processors', '1', 'onFailure', '0', 'onFailure', '0'] }, + }); + + const s6 = reducer(s5, { + type: 'duplicateProcessor', + payload: { source: ['processors', '1', 'onFailure', '0', 'onFailure', '0'] }, + }); + + expect(s6.processors).toEqual([ + processor1, + { + ...processor2, + onFailure: [ + { + ...processor3, + onFailure: [processor4, processor4, processor4], + }, + ], + }, + ]); + }); + + describe('Error conditions', () => { + let originalErrorLogger: any; + beforeEach(() => { + // eslint-disable-next-line no-console + originalErrorLogger = console.error; + // eslint-disable-next-line no-console + console.error = jest.fn(); + }); + + afterEach(() => { + // eslint-disable-next-line no-console + console.error = originalErrorLogger; + }); + + it('prevents moving a parent into child list', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the parent into a child list + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '1'], + destination: ['processors', '1', 'onFailure', '0', 'onFailure', '0', 'onFailure', '0'], + }, + }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledWith(new Error(PARENT_CHILD_NEST_ERROR)); + + // Assert nothing changed + expect(s5.processors).toEqual(s4.processors); + }); + + it('does not remove top level processor and onFailure arrays if they are emptied', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'removeProcessor', + payload: { selector: ['processors', '0'] }, + }); + expect(s2.processors).not.toBe(undefined); + }); + + it('throws for bad move processor', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['onFailure'] }, + }); + + const s3 = reducer(s2, { + type: 'moveProcessor', + payload: { + source: ['onFailure'], + destination: ['processors'], + }, + }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledWith( + new Error('Expected number but received "processors"') + ); + + expect(s3.processors).toEqual(s2.processors); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts new file mode 100644 index 0000000000000..4e069aab8bdd1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { Reducer, useReducer, Dispatch } from 'react'; +import { DeserializeResult } from '../deserialize'; +import { getValue, setValue } from '../utils'; +import { ProcessorInternal, ProcessorSelector } from '../types'; + +import { unsafeProcessorMove, duplicateProcessor } from './utils'; + +export type State = Omit & { + onFailure: ProcessorInternal[]; + isRoot: true; +}; + +export type Action = + | { + type: 'addProcessor'; + payload: { processor: Omit; targetSelector: ProcessorSelector }; + } + | { + type: 'updateProcessor'; + payload: { processor: ProcessorInternal; selector: ProcessorSelector }; + } + | { + type: 'removeProcessor'; + payload: { selector: ProcessorSelector }; + } + | { + type: 'moveProcessor'; + payload: { source: ProcessorSelector; destination: ProcessorSelector }; + } + | { + type: 'duplicateProcessor'; + payload: { + source: ProcessorSelector; + }; + }; + +export type ProcessorsDispatch = Dispatch; + +export const reducer: Reducer = (state, action) => { + if (action.type === 'moveProcessor') { + const { destination, source } = action.payload; + try { + return unsafeProcessorMove(state, source, destination); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return { ...state }; + } + } + + if (action.type === 'removeProcessor') { + const { selector } = action.payload; + const processorsSelector = selector.slice(0, -1); + const parentProcessorSelector = processorsSelector.slice(0, -1); + const idx = parseInt(selector[selector.length - 1], 10); + const processors = getValue(processorsSelector, state); + processors.splice(idx, 1); + const parentProcessor = getValue(parentProcessorSelector, state); + if (!processors.length && selector.length && !(parentProcessor as State).isRoot) { + return setValue(processorsSelector, state, undefined); + } + return setValue(processorsSelector, state, [...processors]); + } + + if (action.type === 'addProcessor') { + const { processor, targetSelector } = action.payload; + if (!targetSelector.length) { + throw new Error('Expected target selector to contain a path, but received an empty array.'); + } + const targetProcessor = getValue( + targetSelector, + state + ); + if (!targetProcessor) { + throw new Error( + `Could not find processor or processors array at ${targetSelector.join('.')}` + ); + } + if (Array.isArray(targetProcessor)) { + return setValue( + targetSelector, + state, + targetProcessor.concat({ ...processor, id: uuid.v4() }) + ); + } else { + const processorWithId = { ...processor, id: uuid.v4() }; + targetProcessor.onFailure = targetProcessor.onFailure + ? targetProcessor.onFailure.concat(processorWithId) + : [processorWithId]; + return setValue(targetSelector, state, targetProcessor); + } + } + + if (action.type === 'updateProcessor') { + const { processor, selector } = action.payload; + const processorsSelector = selector.slice(0, -1); + const idx = parseInt(selector[selector.length - 1], 10); + + if (isNaN(idx)) { + throw new Error(`Expected numeric value, received ${idx}`); + } + + const processors = getValue(processorsSelector, state); + processors[idx] = processor; + return setValue(processorsSelector, state, [...processors]); + } + + if (action.type === 'duplicateProcessor') { + const sourceSelector = action.payload.source; + const sourceProcessor = getValue(sourceSelector, state); + const sourceIdx = parseInt(sourceSelector[sourceSelector.length - 1], 10); + const sourceProcessorsArraySelector = sourceSelector.slice(0, -1); + const sourceProcessorsArray = [ + ...getValue(sourceProcessorsArraySelector, state), + ]; + const copy = duplicateProcessor(sourceProcessor); + sourceProcessorsArray.splice(sourceIdx + 1, 0, copy); + return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray); + } + + return state; +}; + +export const useProcessorsState = (initialState: DeserializeResult) => { + const state = { + ...initialState, + onFailure: initialState.onFailure ?? [], + }; + return useReducer(reducer, { ...state, isRoot: true }); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts new file mode 100644 index 0000000000000..7cb7d076623aa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { State } from './processors_reducer'; +import { ProcessorInternal, ProcessorSelector } from '../types'; +import { DropSpecialLocations } from '../constants'; +import { checkIfSamePath, getValue } from '../utils'; + +export const PARENT_CHILD_NEST_ERROR = 'PARENT_CHILD_NEST_ERROR'; + +export const duplicateProcessor = (sourceProcessor: ProcessorInternal): ProcessorInternal => { + const onFailure = sourceProcessor.onFailure + ? sourceProcessor.onFailure.map((p) => duplicateProcessor(p)) + : undefined; + return { + ...sourceProcessor, + onFailure, + id: uuid.v4(), + options: { + ...sourceProcessor.options, + }, + }; +}; + +export const isChildPath = (a: ProcessorSelector, b: ProcessorSelector) => { + return a.every((pathSegment, idx) => pathSegment === b[idx]); +}; + +/** + * Unsafe! + * + * This function takes a data structure and mutates it in place. + * + * It is convenient for updating the processors (see {@link ProcessorInternal}) + * structure in this way because the structure is recursive. We are moving processors between + * different arrays, removing in one, and adding to another. The end result should be consistent + * with these actions. + * + * @remark + * This function assumes parents cannot be moved into themselves. + */ +export const unsafeProcessorMove = ( + state: State, + source: ProcessorSelector, + destination: ProcessorSelector +): State => { + const pathToSourceArray = source.slice(0, -1); + const pathToDestArray = destination.slice(0, -1); + if (isChildPath(source, destination)) { + throw new Error(PARENT_CHILD_NEST_ERROR); + } + const isXArrayMove = !checkIfSamePath(pathToSourceArray, pathToDestArray); + + // Start by setting up references to objects of interest using our selectors + // At this point, our selectors are consistent with the data passed in. + const sourceProcessors = getValue(pathToSourceArray, state); + const destinationProcessors = getValue(pathToDestArray, state); + const sourceIndex = parseInt(source[source.length - 1], 10); + const sourceProcessor = getValue(pathToSourceArray.slice(0, -1), state); + const processor = sourceProcessors[sourceIndex]; + + const lastDestItem = destination[destination.length - 1]; + let destIndex: number; + if (lastDestItem === DropSpecialLocations.top) { + destIndex = 0; + } else if (lastDestItem === DropSpecialLocations.bottom) { + destIndex = Infinity; + } else if (/^-?[0-9]+$/.test(lastDestItem)) { + destIndex = parseInt(lastDestItem, 10); + } else { + throw new Error(`Expected number but received "${lastDestItem}"`); + } + + if (isXArrayMove) { + // First perform the add operation. + if (destinationProcessors) { + destinationProcessors.splice(destIndex, 0, processor); + } else { + const targetProcessor = getValue(pathToDestArray.slice(0, -1), state); + targetProcessor.onFailure = [processor]; + } + // !! Beyond this point, selectors are no longer usable because we have mutated the data structure! + // Second, we perform the deletion operation + sourceProcessors.splice(sourceIndex, 1); + + // If onFailure is empty, delete the array. + if (!sourceProcessors.length && !((sourceProcessor as unknown) as State).isRoot) { + delete sourceProcessor.onFailure; + } + } else { + destinationProcessors.splice(destIndex, 0, processor); + const targetIdx = sourceIndex > destIndex ? sourceIndex + 1 : sourceIndex; + sourceProcessors.splice(targetIdx, 1); + } + + return { ...state, processors: [...state.processors], onFailure: [...state.onFailure] }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts new file mode 100644 index 0000000000000..153c9e252ccc0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Processor } from '../../../../common/types'; + +import { DeserializeResult } from './deserialize'; +import { ProcessorInternal } from './types'; + +type SerializeArgs = DeserializeResult; + +export interface SerializeResult { + processors: Processor[]; + on_failure?: Processor[]; +} + +const convertProcessorInternalToProcessor = (processor: ProcessorInternal): Processor => { + const { options, onFailure, type } = processor; + const outProcessor = { + [type]: { + ...options, + }, + }; + + if (onFailure?.length) { + outProcessor[type].on_failure = convertProcessors(onFailure); + } else if (onFailure) { + outProcessor[type].on_failure = []; + } + + return outProcessor; +}; + +const convertProcessors = (processors: ProcessorInternal[]) => { + const convertedProcessors = []; + + for (const processor of processors) { + convertedProcessors.push(convertProcessorInternalToProcessor(processor)); + } + return convertedProcessors; +}; + +export const serialize = ({ processors, onFailure }: SerializeArgs): SerializeResult => { + return { + processors: convertProcessors(processors), + on_failure: onFailure?.length ? convertProcessors(onFailure) : undefined, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts new file mode 100644 index 0000000000000..aa39fca29fa8b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OnFormUpdateArg } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { SerializeResult } from './serialize'; +import { ProcessorInfo } from './components/processors_tree'; + +/** + * An array of keys that map to a value in an object + * structure. + * + * For instance: + * ['a', 'b', '0', 'c'] given { a: { b: [ { c: [] } ] } } => [] + * + * Additionally, an empty selector `[]`, is a special indicator + * for the root level. + */ +export type ProcessorSelector = string[]; + +/** @private */ +export interface ProcessorInternal { + id: string; + type: string; + options: { [key: string]: any }; + onFailure?: ProcessorInternal[]; +} + +export { OnFormUpdateArg }; + +export interface FormValidityState { + validate: OnFormUpdateArg['validate']; +} + +export interface OnUpdateHandlerArg extends FormValidityState { + getData: () => SerializeResult; +} + +/** + * The editor can be in different modes. This enables us to hold + * a reference to data dispatch to the reducer (like the {@link ProcessorSelector} + * which will be used to update the in-memory processors data structure. + */ +export type EditorMode = + | { id: 'creatingProcessor'; arg: { selector: ProcessorSelector } } + | { id: 'movingProcessor'; arg: ProcessorInfo } + | { id: 'editingProcessor'; arg: { processor: ProcessorInternal; selector: ProcessorSelector } } + | { id: 'removingProcessor'; arg: { selector: ProcessorSelector } } + | { id: 'idle' }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts new file mode 100644 index 0000000000000..0b7620f517161 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getValue, setValue } from './utils'; + +describe('get and set values', () => { + const testObject = Object.freeze([{ onFailure: [{ onFailure: 1 }] }]); + describe('#getValue', () => { + it('gets a deeply nested value', () => { + expect(getValue(['0', 'onFailure', '0', 'onFailure'], testObject)).toBe(1); + }); + + it('empty array for path returns "root" value', () => { + const result = getValue([], testObject); + expect(result).toEqual(testObject); + // Getting does not create a copy + expect(result).toBe(testObject); + }); + }); + + describe('#setValue', () => { + it('sets a deeply nested value', () => { + const result = setValue(['0', 'onFailure', '0', 'onFailure'], testObject, 2); + expect(result).toEqual([{ onFailure: [{ onFailure: 2 }] }]); + expect(result).not.toBe(testObject); + }); + + it('returns value if no path was provided', () => { + setValue([], testObject, 2); + expect(testObject).toEqual([{ onFailure: [{ onFailure: 1 }] }]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts new file mode 100644 index 0000000000000..49d24e8dc35c3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessorSelector } from './types'; + +type Path = string[]; + +/** + * The below get and set functions are built with an API to make setting + * and getting and setting values more simple. + * + * @remark + * NEVER use these with objects that contain keys created by user input. + */ + +/** + * Given a path, get the value at the path + * + * @remark + * If path is an empty array, return the source. + */ +export const getValue = (path: Path, source: any) => { + let current = source; + for (const key of path) { + current = (current as any)[key]; + } + return (current as unknown) as Result; +}; + +const ARRAY_TYPE = Object.prototype.toString.call([]); +const OBJECT_TYPE = Object.prototype.toString.call({}); + +const dumbCopy = (value: R): R => { + const objectType = Object.prototype.toString.call(value); + if (objectType === ARRAY_TYPE) { + return ([...(value as any)] as unknown) as R; + } else if (objectType === OBJECT_TYPE) { + return { ...(value as any) } as R; + } + + throw new Error(`Expected (${ARRAY_TYPE}|${OBJECT_TYPE}) but received ${objectType}`); +}; + +const WHITELISTED_KEYS_REGEX = /^([0-9]+|onFailure|processors)$/; +/** + * Given a path, value and an object (array or object) set + * the value at the path and copy objects values on the + * path only. This is a partial copy mechanism that is best + * effort for providing state updates to the UI, could break down + * if other updates are made to non-copied parts of state in external + * references - but this should not happen. + * + * @remark + * If path is empty, just shallow copy source. + */ +export const setValue = ( + path: Path, + source: Target, + value: Value +): Target => { + if (!path.length) { + return dumbCopy(source); + } + + let current: any; + let result: Target; + + for (let idx = 0; idx < path.length; ++idx) { + const key = path[idx]; + if (!WHITELISTED_KEYS_REGEX.test(key)) { + // eslint-disable-next-line no-console + console.error( + `Received non-whitelisted key "${key}". Aborting set value operation; returning original.` + ); + return dumbCopy(source); + } + const atRoot = !current; + + if (atRoot) { + result = dumbCopy(source); + current = result; + } + + if (idx + 1 === path.length) { + current[key] = value; + } else { + current[key] = dumbCopy(current[key]); + current = current[key]; + } + } + + return result!; +}; + +export const checkIfSamePath = (pathA: ProcessorSelector, pathB: ProcessorSelector) => { + if (pathA.length !== pathB.length) return false; + return pathA.join('.') === pathB.join('.'); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index ab56ae427120b..9ddb953c71978 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -29,7 +29,13 @@ export { Form, getUseField, ValidationFuncArg, + FormData, + UseField, + FormHook, useFormContext, + FormDataProvider, + OnFormUpdateArg, + FieldConfig, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -41,6 +47,9 @@ export { getFormRow, Field, JsonEditorField, + FormRow, + ToggleField, + ComboBoxField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dca4224592025..a1b0fb8a3de70 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8521,28 +8521,18 @@ "xpack.ingestPipelines.form.nameDescription": "このパイプラインの固有の識別子です。", "xpack.ingestPipelines.form.nameFieldLabel": "名前", "xpack.ingestPipelines.form.nameTitle": "名前", - "xpack.ingestPipelines.form.onFailureDescription": "プロセッサーが失敗した後に実行する代替プロセッサー。{learnMoreLink}", - "xpack.ingestPipelines.form.onFailureDocumentionLink": "詳細", - "xpack.ingestPipelines.form.onFailureFieldAriaLabel": "障害プロセッサーJSONエディター", "xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用:{code}", "xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)", "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。", - "xpack.ingestPipelines.form.onFailureTitle": "障害プロセッサー", - "xpack.ingestPipelines.form.onFailureToggleDescription": "障害プロセッサーを追加", "xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。", - "xpack.ingestPipelines.form.processorsDocumentionLink": "詳細", - "xpack.ingestPipelines.form.processorsFieldAriaLabel": "プロセッサーJSONエディター", - "xpack.ingestPipelines.form.processorsFieldDescription": "インデックスの前にドキュメントを変換するために使用するプロセッサー。{learnMoreLink}", "xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用:{code}", "xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー", - "xpack.ingestPipelines.form.processorsFieldTitle": "プロセッサー", "xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。", "xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。", "xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存", "xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません", "xpack.ingestPipelines.form.savingButtonLabel": "保存中…", "xpack.ingestPipelines.form.showRequestButtonLabel": "リクエストを表示", - "xpack.ingestPipelines.form.testPipelineButtonLabel": "パイプラインをテスト", "xpack.ingestPipelines.form.versionFieldLabel": "バージョン(任意)", "xpack.ingestPipelines.form.versionToggleDescription": "バージョン番号を追加", "xpack.ingestPipelines.licenseCheckErrorMessage": "ライセンス確認失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2fb11c2252b4e..6047de1deb12a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8525,28 +8525,18 @@ "xpack.ingestPipelines.form.nameDescription": "此管道的唯一标识符。", "xpack.ingestPipelines.form.nameFieldLabel": "名称", "xpack.ingestPipelines.form.nameTitle": "名称", - "xpack.ingestPipelines.form.onFailureDescription": "处理器失败后要执行的备用处理器。{learnMoreLink}", - "xpack.ingestPipelines.form.onFailureDocumentionLink": "了解详情", - "xpack.ingestPipelines.form.onFailureFieldAriaLabel": "失败处理器 JSON 编辑器", "xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}", "xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)", "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。", - "xpack.ingestPipelines.form.onFailureTitle": "失败处理器", - "xpack.ingestPipelines.form.onFailureToggleDescription": "添加失败处理器", "xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。", - "xpack.ingestPipelines.form.processorsDocumentionLink": "了解详情", - "xpack.ingestPipelines.form.processorsFieldAriaLabel": "处理器 JSON 编辑器", - "xpack.ingestPipelines.form.processorsFieldDescription": "用于在索引之前转换文档的处理器。{learnMoreLink}", "xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}", "xpack.ingestPipelines.form.processorsFieldLabel": "处理器", - "xpack.ingestPipelines.form.processorsFieldTitle": "处理器", "xpack.ingestPipelines.form.processorsJsonError": "输入无效。", "xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。", "xpack.ingestPipelines.form.saveButtonLabel": "保存管道", "xpack.ingestPipelines.form.savePipelineError": "无法创建管道", "xpack.ingestPipelines.form.savingButtonLabel": "正在保存......", "xpack.ingestPipelines.form.showRequestButtonLabel": "显示请求", - "xpack.ingestPipelines.form.testPipelineButtonLabel": "测试管道", "xpack.ingestPipelines.form.versionFieldLabel": "版本(可选)", "xpack.ingestPipelines.form.versionToggleDescription": "添加版本号", "xpack.ingestPipelines.licenseCheckErrorMessage": "许可证检查失败", diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index e926bcd6ef997..353902f4265a0 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -11,22 +11,6 @@ const PIPELINE = { name: 'test_pipeline', description: 'My pipeline description.', version: 1, - processors: JSON.stringify([ - { - set: { - field: 'foo', - value: 'new', - }, - }, - ]), - onFailureProcessors: JSON.stringify([ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ]), }; export default ({ getPageObjects, getService }: FtrProviderContext) => { From 2ebe35ff15e9eebd4bbd7e3d82c99fa95db7749d Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 16 Jun 2020 10:21:19 -0400 Subject: [PATCH 13/68] [ML] DF Analytics Results: do not hide query bar and table on query syntax error (#69196) * do not hide query bar and table if query syntax error * check for query error in analytics exploration --- .../exploration_results_table/exploration_results_table.tsx | 3 ++- .../components/outlier_exploration/outlier_exploration.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 24e5785c6e808..105eb9f73804d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -74,8 +74,9 @@ export const ExplorationResultsTable: FC = React.memo( if (jobConfig === undefined || classificationData === undefined) { return null; } + // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 58f8528236bb9..917ab1b0ed1dd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -56,7 +56,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { return ( From 798c520985a0521575dff1679f70ef4872c3184b Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 16 Jun 2020 10:32:41 -0400 Subject: [PATCH 14/68] [Ingest Manager] Use search param vs KQL for events count (#69209) * Replace filter with search in events count Co-authored-by: Elastic Machine --- .../plugins/ingest_manager/server/services/agents/status.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 63388db890ea5..0efb202eff532 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -56,9 +56,8 @@ export async function getAgentStatusForConfig( async function getEventsCount(soClient: SavedObjectsClientContract, configId?: string) { const { total } = await soClient.find({ type: AGENT_EVENT_SAVED_OBJECT_TYPE, - filter: configId - ? `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.config_id:"${configId}"` - : undefined, + searchFields: ['config_id'], + search: configId, perPage: 0, page: 1, sortField: 'timestamp', From 7beabd2bef309098279492a584098e0160e6ac40 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 16 Jun 2020 10:33:26 -0400 Subject: [PATCH 15/68] [Ingest Manager] Use search param vs KQL to find default agent (#69211) * Remove another instance of filter for is_default Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/server/services/agent_config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 4a877ef7de13b..18d5d8dedfb1f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -73,7 +73,8 @@ class AgentConfigService { public async ensureDefaultAgentConfig(soClient: SavedObjectsClientContract) { const configs = await soClient.find({ type: AGENT_CONFIG_SAVED_OBJECT_TYPE, - filter: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.attributes.is_default:true`, + searchFields: ['is_default'], + search: 'true', }); if (configs.total === 0) { From 461d44d8a49dcc5f1e7453d57555432b5bc8481e Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 16 Jun 2020 08:41:59 -0600 Subject: [PATCH 16/68] [SIEM][Detection Engine] Fixes a stale view/query within the signals table when re-opening/closing signals ## Summary Fixes a one liner bug where we were not using waitFor and could have stale views of data from the timeline. This removes that by adding a `waitFor` when setting the signals to be closed, open, in-progress, etc... This also adds a new `waitFor` for the end to end tests and fixes other tests to use that rather than doing a wait for 5 seconds. This should keep the end to end backend tests fast. Before this you could sometimes try to re-open 3 signals like below and it would not change the signals to being open but rather re-query the stale view and show the same signals as being closed when they are not closed: ![crop_bug](https://user-images.githubusercontent.com/1151048/84713572-8b5e7f00-af28-11ea-80b6-2ad67f16b7df.gif) Now with this PR, those signals will show as being re-opened and the closed signals will update correctly within the same UI on the same tab. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../schemas/common/schemas.ts | 1 + .../signals/open_close_signals_route.ts | 1 + .../basic/tests/find_statuses.ts | 13 +- .../basic/tests/import_rules.ts | 9 +- .../basic/tests/open_close_signals.ts | 241 +++++++++++++++++- .../security_and_spaces/tests/create_rules.ts | 11 +- .../tests/create_rules_bulk.ts | 10 +- .../tests/find_statuses.ts | 10 +- .../security_and_spaces/tests/import_rules.ts | 9 +- .../tests/open_close_signals.ts | 219 +++++++++++++++- .../detection_engine_api_integration/utils.ts | 46 +++- 11 files changed, 558 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 93883eeff5271..0c7bcdefd360d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -235,6 +235,7 @@ export const pageOrUndefined = t.union([page, t.undefined]); export type PageOrUndefined = t.TypeOf; export const signal_ids = t.array(t.string); +export type SignalIds = t.TypeOf; // TODO: Can this be more strict or is this is the set of all Elastic Queries? export const signal_status_query = t.object; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 18c3440ab93e1..ac5132d93a069 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -55,6 +55,7 @@ export const setSignalsStatusRoute = (router: IRouter) => { try { const result = await clusterClient.callAsCurrentUser('updateByQuery', { index: siemClient.getSignalsIndex(), + refresh: 'wait_for', body: { script: { source: `ctx._source.signal.status = '${status}'`, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts index 6ff5a8c552ab7..cc6fa53939f60 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts @@ -14,6 +14,7 @@ import { deleteSignalsIndex, deleteAllRulesStatuses, getSimpleRule, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -51,7 +52,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // wait for Task Manager to execute the rule and update status - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return ( + body[resBody.id].current_status?.status === 'succeeded' || + body[resBody.id].current_status?.status === 'going to run' + ); + }); // query the single rule from _find const { body } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts index 66962df14e22d..e0b60ae1fbeeb 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -17,6 +17,7 @@ import { getSimpleRuleOutput, removeServerGeneratedProperties, ruleToNdjson, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -33,8 +34,12 @@ export default ({ getService }: FtrProviderContext): void => { .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(400); - // We have to wait up to 5 seconds for any unresolved promises to flush - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send(); + return body.status_code === 404; + }); // Try to fetch the rule which should still be a 404 (not found) const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts index fe7783fa2d045..93dbc2471ae18 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -6,18 +6,31 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../plugins/security_solution/common/constants'; +import { SearchResponse } from 'elasticsearch'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, deleteSignalsIndex, setSignalStatus, getSignalStatusEmptyResponse, + getSimpleRule, + waitFor, + getQueryAllSignals, + getQuerySignalIds, + deleteAllAlerts, } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); describe('open_close_signals', () => { describe('validation checks', () => { @@ -50,5 +63,231 @@ export default ({ getService }: FtrProviderContext) => { await deleteSignalsIndex(supertest); }); }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await deleteAllAlerts(es); + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute and get 10 signals', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get the collection of signals + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be have set the signals in an open state initially', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get the collection of signals + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const everySignalOpen = signalsOpen.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'open' + ); + + // expect their initial state to all be open + expect(everySignalOpen).to.eql(true); + }); + + it('should be able to get a count of 10 closed signals when closing 10', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get a collection of signals to get the id to set open/closed on them + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // set all of the signals to the state of closed. There is no reason to use a waitUntil here + // as this route intentionally has a waitFor within it and should only return when the query has + // the data. + await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(200); + + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + expect(signalsClosed.hits.hits.length).to.equal(10); + }); + + it('should be able close 10 signals immediately and they all should be closed', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get a collection of signals to get the id to set open/closed on them + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // set all of the signals to the state of closed. There is no reason to use a waitUntil here + // as this route intentionally has a waitFor within it and should only return when the query has + // the data. + await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(200); + + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + const everySignalClosed = signalsClosed.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'closed' + ); + expect(everySignalClosed).to.eql(true); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 8847b0c3250ac..c763be1c2c3ec 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -23,6 +23,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleMlRule, getSimpleMlRuleOutput, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -96,7 +97,15 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // wait for Task Manager to execute the rule and update status - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + return statusBody[body.id].current_status?.status === 'succeeded'; + }); + const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index 47b09fd1fe3c7..52865e43be750 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -21,6 +21,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -99,7 +100,14 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // wait for Task Manager to execute the rule and update status - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body[0].id] }) + .expect(200); + return statusBody[body[0].id].current_status?.status === 'succeeded'; + }); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts index f659857b89a97..aee8d400be905 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -14,6 +14,7 @@ import { deleteSignalsIndex, deleteAllRulesStatuses, getSimpleRule, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -70,7 +71,14 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // wait for Task Manager to execute the rule and update status - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id].current_status?.status === 'succeeded'; + }); // query the single rule from _find const { body } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index 66962df14e22d..e0b60ae1fbeeb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -17,6 +17,7 @@ import { getSimpleRuleOutput, removeServerGeneratedProperties, ruleToNdjson, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -33,8 +34,12 @@ export default ({ getService }: FtrProviderContext): void => { .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') .expect(400); - // We have to wait up to 5 seconds for any unresolved promises to flush - await new Promise((resolve) => setTimeout(resolve, 5000)); + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send(); + return body.status_code === 404; + }); // Try to fetch the rule which should still be a 404 (not found) const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index fe7783fa2d045..6b924eddfdc44 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -6,18 +6,31 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../plugins/security_solution/common/constants'; +import { SearchResponse } from 'elasticsearch'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, deleteSignalsIndex, setSignalStatus, getSignalStatusEmptyResponse, + getSimpleRule, + waitFor, + getQueryAllSignals, + getQuerySignalIds, + deleteAllAlerts, } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); describe('open_close_signals', () => { describe('validation checks', () => { @@ -49,6 +62,210 @@ export default ({ getService }: FtrProviderContext) => { await deleteSignalsIndex(supertest); }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await deleteAllAlerts(es); + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute and get 10 signals', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get the collection of signals + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be have set the signals in an open state initially', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get the collection of signals + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const everySignalOpen = signalsOpen.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'open' + ); + + // expect their initial state to all be open + expect(everySignalOpen).to.eql(true); + }); + + it('should be able to get a count of 10 closed signals when closing 10', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get a collection of signals to get the id to set open/closed on them + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // set all of the signals to the state of closed. There is no reason to use a waitUntil here + // as this route intentionally has a waitFor within it and should only return when the query has + // the data. + await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(200); + + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + expect(signalsClosed.hits.hits.length).to.equal(10); + }); + + it('should be able close 10 signals immediately and they all should be closed', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until all the rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length === 10; + }); + + // Get a collection of signals to get the id to set open/closed on them + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // set all of the signals to the state of closed. There is no reason to use a waitUntil here + // as this route intentionally has a waitFor within it and should only return when the query has + // the data. + await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(200); + + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + const everySignalClosed = signalsClosed.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'closed' + ); + expect(everySignalClosed).to.eql(true); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index b58089e30e930..816df9c133ea1 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -7,6 +7,10 @@ import { Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { + Status, + SignalIds, +} from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; @@ -106,12 +110,24 @@ export const getSignalStatus = () => ({ aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, }); +export const getQueryAllSignals = () => ({ + query: { match_all: {} }, +}); + +export const getQuerySignalIds = (signalIds: SignalIds) => ({ + query: { + terms: { + _id: signalIds, + }, + }, +}); + export const setSignalStatus = ({ signalIds, status, }: { - signalIds: string[]; - status: 'open' | 'closed'; + signalIds: SignalIds; + status: Status; }) => ({ signal_ids: signalIds, status, @@ -457,3 +473,29 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => query: 'user.name: root or user.name: admin', exceptions_list: [], }); + +// Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor +export const waitFor = async ( + functionToTest: () => Promise, + maxTimeout: number = 5000, + timeoutWait: number = 10 +) => { + await new Promise(async (resolve, reject) => { + let found = false; + let numberOfTries = 0; + while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) { + const itPasses = await functionToTest(); + if (itPasses) { + found = true; + } else { + numberOfTries++; + } + await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait)); + } + if (found) { + resolve(); + } else { + reject(new Error('timed out waiting for function condition to be true')); + } + }); +}; From b42defa37e5d1292fa067f80fceadb298c2d8bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 16 Jun 2020 16:17:17 +0100 Subject: [PATCH 17/68] [Observability] Update landing page copy and content (#69247) * removing try it section * removing try it section --- .../observability/public/pages/home/index.tsx | 43 +------------------ .../public/pages/home/section.ts | 22 ---------- 2 files changed, 1 insertion(+), 64 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 072d3c47d3a55..696361393ef82 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -10,7 +10,6 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiIcon, EuiImage, EuiSpacer, @@ -21,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import styled from 'styled-components'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { appsSection, tryItOutItemsSection } from './section'; +import { appsSection } from './section'; const Container = styled.div` min-height: calc(100vh - 48px); @@ -158,46 +157,6 @@ export const Home = () => { - - - - {/* Try it out */} - - - - -

- {i18n.translate('xpack.observability.home.tryItOut', { - defaultMessage: 'Try it out', - })} -

-
-
-
-
- - {/* Try it out sections */} - - - {tryItOutItemsSection.map((item) => ( - - } - title={ - -

{item.title}

-
- } - description={item.description} - target={item.target} - href={item.href} - /> -
- ))} -
- -
diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts index f8bbfbfa30548..a2b82c31bf2ab 100644 --- a/x-pack/plugins/observability/public/pages/home/section.ts +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -60,25 +60,3 @@ export const appsSection: ISection[] = [ }), }, ]; - -export const tryItOutItemsSection: ISection[] = [ - { - id: 'demo', - title: i18n.translate('xpack.observability.section.tryItOut.demo.title', { - defaultMessage: 'Demo Playground', - }), - icon: 'play', - description: '', - href: 'https://demo.elastic.co/', - target: '_blank', - }, - { - id: 'sampleData', - title: i18n.translate('xpack.observability.section.tryItOut.sampleData.title', { - defaultMessage: 'Add sample data', - }), - icon: 'documents', - description: '', - href: '/app/home#/tutorial_directory/sampleData', - }, -]; From f43d51d41bd9cf347db8064297b9fe7ebd347801 Mon Sep 17 00:00:00 2001 From: Vignesh Shanmugam Date: Tue, 16 Jun 2020 17:24:40 +0200 Subject: [PATCH 18/68] feat: instrument navigation changes using RUM agent (#67633) * feat: instrument navigation changes using RUM agent * chore: rebase and change application contract * chore: fix type tests * docs: update public.md doc * chore: remove internal application export --- .github/CODEOWNERS | 3 +-- package.json | 2 +- src/core/public/core_system.ts | 4 +++ src/core/public/kbn_bootstrap.ts | 30 +++++++++++++++++++--- src/core/public/public.api.md | 8 +++++- src/legacy/ui/apm/index.js | 10 ++------ src/legacy/ui/ui_render/ui_render_mixin.js | 2 +- x-pack/package.json | 2 +- yarn.lock | 28 ++++++++++---------- 9 files changed, 57 insertions(+), 32 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 00573e04396b4..681564b44297f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -66,11 +66,10 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui -/x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui -/src/apm.js @watson +/src/apm.js @watson @vigneshshanmugam # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats diff --git a/package.json b/package.json index 89f380e5a7a0a..b886e6ae0f5d3 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "@babel/core": "^7.10.2", "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@babel/register": "^7.10.1", - "@elastic/apm-rum": "^5.1.1", + "@elastic/apm-rum": "^5.2.0", "@elastic/charts": "19.2.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index aa52212344f4d..d6172b77d3ca5 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -300,6 +300,10 @@ export class CoreSystem { plugins: mapToObject(plugins.contracts), targetDomElement: rendering.legacyTargetDomElement, }); + + return { + application, + }; } catch (error) { if (this.fatalErrorsSetup) { this.fatalErrorsSetup.add(error); diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index caeb95a540de3..0f86061816701 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -38,11 +38,17 @@ export function __kbnBootstrap__() { * `apmConfig` would be populated with relavant APM RUM agent * configuration if server is started with `ELASTIC_APM_ACTIVE=true` */ - if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && injectedMetadata.vars.apmConfig != null) { + const apmConfig = injectedMetadata.vars.apmConfig; + const APM_ENABLED = process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && apmConfig != null; + + if (APM_ENABLED) { // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-var-requires - const { init } = require('@elastic/apm-rum'); - init(injectedMetadata.vars.apmConfig); + const { init, apm } = require('@elastic/apm-rum'); + if (apmConfig.globalLabels) { + apm.addLabels(apmConfig.globalLabels); + } + init(apmConfig); } i18n @@ -60,6 +66,22 @@ export function __kbnBootstrap__() { setup.fatalErrors.add(i18nError); } - await coreSystem.start(); + const start = await coreSystem.start(); + + if (APM_ENABLED && start) { + /** + * Register listeners for navigation changes and capture them as + * route-change transactions after Kibana app is bootstrapped + */ + start.application.currentAppId$.subscribe((appId) => { + const apmInstance = (window as any).elasticApm; + if (appId && apmInstance && typeof apmInstance.startTransaction === 'function') { + apmInstance.startTransaction(`/app/${appId}`, 'route-change', { + managed: true, + canReuse: true, + }); + } + }); + } }); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 1914fa7be89ec..0fdb5e415ec0e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -562,7 +562,9 @@ export class CoreSystem { fatalErrors: FatalErrorsSetup; } | undefined>; // (undocumented) - start(): Promise; + start(): Promise<{ + application: InternalApplicationStart; + } | undefined>; // (undocumented) stop(): void; } @@ -1587,4 +1589,8 @@ export interface UserProvidedValues { } +// Warnings were encountered during analysis: +// +// src/core/public/core_system.ts:216:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts + ``` diff --git a/src/legacy/ui/apm/index.js b/src/legacy/ui/apm/index.js index f1074091e2624..c43b7b01d1159 100644 --- a/src/legacy/ui/apm/index.js +++ b/src/legacy/ui/apm/index.js @@ -30,21 +30,15 @@ export function apmInit(config) { return apmEnabled ? `init(${config})` : ''; } -export function getApmConfig(app) { +export function getApmConfig(requestPath) { if (!apmEnabled) { return null; } - /** - * we use the injected app metadata from the server to extract the - * app id to be used for page-load transaction - */ - const appId = app.getId(); - const config = { ...getConfig('kibana-frontend'), ...{ active: true, - pageLoadTransactionName: appId, + pageLoadTransactionName: requestPath, }, }; /** diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 673e19155879a..b4f8255297240 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -241,7 +241,7 @@ export function uiRenderMixin(kbnServer, server, config) { savedObjects.getClient(h.request) ); const vars = await legacy.getVars(app.getId(), h.request, { - apmConfig: getApmConfig(app), + apmConfig: getApmConfig(h.request.path), ...overrides, }); const content = await rendering.render(h.request, uiSettings, { diff --git a/x-pack/package.json b/x-pack/package.json index 172a6bc30125b..c582cc8be9bbd 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -188,7 +188,7 @@ "@babel/core": "^7.10.2", "@babel/register": "^7.10.1", "@babel/runtime": "^7.10.2", - "@elastic/apm-rum-react": "^1.1.1", + "@elastic/apm-rum-react": "^1.1.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", diff --git a/yarn.lock b/yarn.lock index 14e4cdd62955a..0188957073965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2076,29 +2076,29 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@elastic/apm-rum-core@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.2.0.tgz#2ed30dc226c9b5779532ab2e6065a155587bcea4" - integrity sha512-3ti2dhrqfxjHFXgArQI/sVAG2AgZH0kB1nx+2WjLpuAh8gGS4R772M5VXcWcGQb8UW9jrANwwbW2hT2GKv+uOA== +"@elastic/apm-rum-core@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.3.0.tgz#3ae5e84eba5b5287b92458a49755f6e39e7bba5b" + integrity sha512-b/qAnPqi3km808BhSYo+ROpTINm3eVBQ6hNcxOELwKitS3O/HikkwRn5aPkVIhQXOVrbPSufMl1A991nrE3daA== dependencies: error-stack-parser "^1.3.5" opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.1.1.tgz#3a2ba91efc9260da55ef6c31cce642a476eba828" - integrity sha512-ZMixw+82VbZIDBnz0dj5Fo4PZ7pnXlLjAA7XTi3AtSIEjpsZa7YIuCFzJdrgb/nOq7MOFkODkFPTiIYAM/yCqg== +"@elastic/apm-rum-react@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.1.2.tgz#274cc414074d05e33e8f0afcad25ef9a30d99452" + integrity sha512-2/wEaPF4EQaVzU8Qj5aYucDc+VFr7438AieON31fx8wsbvnxh9iG+iV7xky2YtT/mf53BbFgZm35L5y/pxCKwA== dependencies: - "@elastic/apm-rum" "^5.1.1" + "@elastic/apm-rum" "^5.2.0" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.1.1.tgz#02d29fa606e66f9a3a88f629d468b02457b1b183" - integrity sha512-A0O/0ZffcHm1taLuXyFoUmV2+aARr+9+xbmsEDIExLV0yKaNlaLl3UaZrodSOZ1ijlCEsRRS+y2i0md93YKQTA== +"@elastic/apm-rum@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.2.0.tgz#b0cfd6e5771b1e765fda2715a38c87746f49f1aa" + integrity sha512-l8/Ji1GMuahMCN5DsALIf+fioKi1QeY4pU0izfVI37se2/fxsMNEDpw52WxJknHdfBE0Imh3FPg4T56J5MO+IQ== dependencies: - "@elastic/apm-rum-core" "^5.2.0" + "@elastic/apm-rum-core" "^5.3.0" "@elastic/charts@19.2.0": version "19.2.0" From a9d73e868395c7d29ee5b96a38756b758ae6e802 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 16 Jun 2020 18:04:52 +0200 Subject: [PATCH 19/68] [Uptime] Monitor availability reporting (#67790) Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../uptime/common/runtime_types/common.ts | 15 +- .../common/runtime_types/monitor/locations.ts | 12 +- .../components/certificates/translations.ts | 4 + .../public/components/common/translations.ts | 11 + .../uptime/public/components/monitor/index.ts | 4 +- .../__snapshots__/location_map.test.tsx.snap | 282 ----- .../location_status_tags.test.tsx.snap | 682 ----------- .../monitor/location_map/location_map.tsx | 95 -- .../location_map/location_status_tags.tsx | 130 -- .../monitor_status.bar.test.tsx.snap | 55 - .../monitor_status_bar/ssl_certificate.tsx | 100 -- .../monitor_status_bar/status_bar.tsx | 61 - .../monitor_status_details/translations.ts | 50 - .../monitor_status.bar.test.tsx.snap | 73 ++ .../ssl_certificate.test.tsx.snap | 114 +- .../status_by_location.test.tsx.snap | 0 .../__test__/monitor_status.bar.test.tsx | 28 +- .../__test__/ssl_certificate.test.tsx | 24 +- .../__test__/status_by_location.test.tsx | 30 + .../availability_reporting.test.tsx.snap | 381 ++++++ .../location_status_tags.test.tsx.snap | 1085 +++++++++++++++++ .../__snapshots__/tag_label.test.tsx.snap | 56 + .../__tests__/availability_reporting.test.tsx | 42 + .../__tests__/location_status_tags.test.tsx | 32 +- .../__tests__/tag_label.test.tsx | 21 + .../availability_reporting.tsx | 87 ++ .../availability_reporting/index.ts | 9 + .../location_status_tags.tsx | 73 ++ .../availability_reporting/tag_label.tsx | 35 + .../index.ts | 6 +- .../location_availability.test.tsx.snap | 234 ++++ .../__tests__/location_availability.test.tsx} | 52 +- .../location_availability.tsx | 90 ++ .../location_availability/toggle_view_btn.tsx | 61 + .../use_selected_view.ts | 26 + .../__snapshots__/location_map.test.tsx.snap | 27 + .../location_missing.test.tsx.snap | 1 + .../__tests__/location_map.test.tsx | 33 + .../__tests__/location_missing.test.tsx | 0 .../embeddables/__tests__/__mocks__/mock.ts | 6 + .../embeddables/__tests__/map_config.test.ts | 14 +- .../location_map/embeddables/embedded_map.tsx | 55 +- .../embeddables/low_poly_layer.json | 0 .../location_map/embeddables/map_config.ts | 8 +- .../location_map/embeddables/map_tool_tip.tsx | 90 ++ .../location_map/embeddables/translations.ts | 0 .../location_map/index.ts | 2 +- .../location_map/location_map.tsx | 34 + .../location_map/location_missing.tsx | 3 +- .../status_bar}/index.ts | 3 +- .../status_bar/ssl_certificate.tsx | 41 + .../status_details/status_bar/status_bar.tsx | 82 ++ .../status_bar}/status_by_location.tsx | 0 .../status_bar/use_status_bar.ts} | 31 +- .../status_details.tsx | 27 +- .../status_details_container.tsx | 4 +- .../translations.ts | 53 + .../monitor_list/cert_status_column.tsx | 31 +- .../overview/monitor_list/monitor_list.tsx | 3 +- .../overview/monitor_list/translations.ts | 4 - .../__snapshots__/page_header.test.tsx.snap | 40 +- .../uptime/public/pages/page_header.tsx | 7 +- .../lib/requests/get_monitor_locations.ts | 27 +- x-pack/test/accessibility/apps/uptime.ts | 2 +- .../test/functional/apps/uptime/locations.ts | 73 +- .../test/functional/services/uptime/common.ts | 4 +- .../functional/services/uptime/monitor.ts | 11 + .../functional/services/uptime/navigation.ts | 9 + 70 files changed, 3125 insertions(+), 1672 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/common/translations.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/__test__/__snapshots__/ssl_certificate.test.tsx.snap (61%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/__test__/__snapshots__/status_by_location.test.tsx.snap (100%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/__test__/monitor_status.bar.test.tsx (70%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/__test__/ssl_certificate.test.tsx (75%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/__test__/status_by_location.test.tsx (77%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx rename x-pack/plugins/uptime/public/components/monitor/{location_map => status_details/availability_reporting}/__tests__/location_status_tags.test.tsx (84%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/index.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/index.ts (63%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/__snapshots__/location_availability.test.tsx.snap rename x-pack/plugins/uptime/public/components/monitor/{location_map/__tests__/location_map.test.tsx => status_details/location_availability/__tests__/location_availability.test.tsx} (64%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_map.test.tsx.snap rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap (99%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/location_map.test.tsx rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/__tests__/location_missing.test.tsx (100%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/__tests__/__mocks__/mock.ts (96%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/__tests__/map_config.test.ts (65%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/embedded_map.tsx (68%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/low_poly_layer.json (100%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/map_config.ts (94%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/embeddables/translations.ts (100%) rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/index.ts (81%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx rename x-pack/plugins/uptime/public/components/monitor/{ => status_details}/location_map/location_missing.tsx (96%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details/monitor_status_bar => status_details/status_bar}/index.ts (72%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details/monitor_status_bar => status_details/status_bar}/status_by_location.tsx (100%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details/monitor_status_bar/status_bar_container.tsx => status_details/status_bar/use_status_bar.ts} (66%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/status_details.tsx (72%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details => status_details}/status_details_container.tsx (92%) rename x-pack/plugins/uptime/public/components/monitor/{monitor_status_details/monitor_status_bar => status_details}/translations.ts (50%) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a1b0fb8a3de70..7b887867b43b8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16606,7 +16606,6 @@ "xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。", "xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。", "xpack.uptime.locationMap.locations.missing.title": "地理情報の欠測", - "xpack.uptime.locationMap.locations.tags.others": "{otherLoc}その他 ...", "xpack.uptime.locationName.helpLinkAnnotation": "場所を追加", "xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知", @@ -16690,12 +16689,7 @@ "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "{loc}場所での{status}", "xpack.uptime.monitorStatusBar.locations.upStatus": "{loc}場所での{status}", "xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "監視 URL リンク", - "xpack.uptime.monitorStatusBar.sslCertificate.overview": "証明書概要", "xpack.uptime.monitorStatusBar.sslCertificate.title": "証明書", - "xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent": "{emphasizedText}が期限切れになりました", - "xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel": "{validityDate}に期限切れになりました", - "xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent": "{emphasizedText}が期限切れになります", - "xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel": "{validityDate}に期限切れになります", "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間", "xpack.uptime.navigateToAlertingButton.content": "アラートを管理", "xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6047de1deb12a..e821c4fb22899 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16612,7 +16612,6 @@ "xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。", "xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。", "xpack.uptime.locationMap.locations.missing.title": "地理信息缺失", - "xpack.uptime.locationMap.locations.tags.others": "{otherLoc} 其他......", "xpack.uptime.locationName.helpLinkAnnotation": "添加位置", "xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测", @@ -16696,12 +16695,7 @@ "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置{status}", "xpack.uptime.monitorStatusBar.locations.upStatus": "在 {loc} 位置{status}", "xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "监测 URL 链接", - "xpack.uptime.monitorStatusBar.sslCertificate.overview": "证书概览", "xpack.uptime.monitorStatusBar.sslCertificate.title": "证书", - "xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent": "{emphasizedText}过期", - "xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel": "已于 {validityDate}过期", - "xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent": "{emphasizedText}过期", - "xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel": "将于 {validityDate}过期", "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间", "xpack.uptime.navigateToAlertingButton.content": "管理告警", "xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面", diff --git a/x-pack/plugins/uptime/common/runtime_types/common.ts b/x-pack/plugins/uptime/common/runtime_types/common.ts index e07c46fa01cfe..603d242d4dcd0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/common.ts +++ b/x-pack/plugins/uptime/common/runtime_types/common.ts @@ -6,15 +6,19 @@ import * as t from 'io-ts'; -export const LocationType = t.partial({ +export const LocationType = t.type({ lat: t.string, lon: t.string, }); -export const CheckGeoType = t.partial({ - name: t.string, - location: LocationType, -}); +export const CheckGeoType = t.intersection([ + t.type({ + name: t.string, + }), + t.partial({ + location: LocationType, + }), +]); export const SummaryType = t.partial({ up: t.number, @@ -34,5 +38,6 @@ export const DateRangeType = t.type({ export type Summary = t.TypeOf; export type Location = t.TypeOf; +export type GeoPoint = t.TypeOf; export type StatesIndexStatus = t.TypeOf; export type DateRange = t.TypeOf; diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts index ea3cfe677ca99..00ed1dc407e98 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts @@ -7,17 +7,23 @@ import * as t from 'io-ts'; import { CheckGeoType, SummaryType } from '../common'; // IO type for validation -export const MonitorLocationType = t.partial({ +export const MonitorLocationType = t.type({ + up_history: t.number, + down_history: t.number, + timestamp: t.string, summary: SummaryType, geo: CheckGeoType, - timestamp: t.string, }); // Typescript type for type checking export type MonitorLocation = t.TypeOf; export const MonitorLocationsType = t.intersection([ - t.type({ monitorId: t.string }), + t.type({ + monitorId: t.string, + up_history: t.number, + down_history: t.number, + }), t.partial({ locations: t.array(MonitorLocationType) }), ]); export type MonitorLocations = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/certificates/translations.ts b/x-pack/plugins/uptime/public/components/certificates/translations.ts index 518eddf1211a4..176625d647ca0 100644 --- a/x-pack/plugins/uptime/public/components/certificates/translations.ts +++ b/x-pack/plugins/uptime/public/components/certificates/translations.ts @@ -18,6 +18,10 @@ export const EXPIRES_SOON = i18n.translate('xpack.uptime.certs.expireSoon', { defaultMessage: 'Expires soon', }); +export const EXPIRES = i18n.translate('xpack.uptime.certs.expires', { + defaultMessage: 'Expires', +}); + export const SEARCH_CERTS = i18n.translate('xpack.uptime.certs.searchCerts', { defaultMessage: 'Search certificates', }); diff --git a/x-pack/plugins/uptime/public/components/common/translations.ts b/x-pack/plugins/uptime/public/components/common/translations.ts new file mode 100644 index 0000000000000..d2c466ddf0c83 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const URL_LABEL = i18n.translate('xpack.uptime.monitorList.table.url.name', { + defaultMessage: 'Url', +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts index cb7b27afded02..fd9a9a2c897d7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/index.ts @@ -6,7 +6,7 @@ export * from './ml'; export * from './ping_list'; -export * from './location_map'; -export * from './monitor_status_details'; +export * from './status_details/location_map'; +export * from './status_details'; export * from './ping_histogram'; export * from './monitor_charts'; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap deleted file mode 100644 index 7b847782fe1ac..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap +++ /dev/null @@ -1,282 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMap component doesnt shows warning if geo is provided 1`] = ` - - - - - - - - - - - - - - -`; - -exports[`LocationMap component renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - - - -`; - -exports[`LocationMap component renders named locations that have missing geo data 1`] = ` - - - - - - - - - - - - - - - -`; - -exports[`LocationMap component shows warning if geo information is missing 1`] = ` - - - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap deleted file mode 100644 index f2a390792918a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap +++ /dev/null @@ -1,682 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationStatusTags component renders properly against props 1`] = ` - - - - - - - - Berlin - - - - - - 1 Mon ago - - - - - - - - - - Berlin - - - - - - 1 Mon ago - - - - - - - - Islamabad - - - - - - 1 Mon ago - - - - - - -`; - -exports[`LocationStatusTags component renders when all locations are down 1`] = ` -.c3 { - display: inline-block; - margin-left: 4px; -} - -.c2 { - font-weight: 600; -} - -.c1 { - margin-bottom: 5px; - white-space: nowrap; -} - -.c0 { - max-height: 229px; - overflow: hidden; - margin-top: auto; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
- -
- - - -
-
- Islamabad -
-
-
-
-
- -
-
- 5s ago -
-
-
-
-
- - - -
-
- Berlin -
-
-
-
-
- -
-
- 5m ago -
-
-
-
-
- -
-`; - -exports[`LocationStatusTags component renders when all locations are up 1`] = ` -.c3 { - display: inline-block; - margin-left: 4px; -} - -.c2 { - font-weight: 600; -} - -.c1 { - margin-bottom: 5px; - white-space: nowrap; -} - -.c0 { - max-height: 229px; - overflow: hidden; - margin-top: auto; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
- - -
- - - -
-
- Berlin -
-
-
-
-
- -
-
- 5d ago -
-
-
-
-
- - - -
-
- Islamabad -
-
-
-
-
- -
-
- 5s ago -
-
-
-
-
-
-`; - -exports[`LocationStatusTags component renders when there are many location 1`] = ` -Array [ - .c3 { - display: inline-block; - margin-left: 4px; -} - -.c2 { - font-weight: 600; -} - -.c1 { - margin-bottom: 5px; - white-space: nowrap; -} - -.c0 { - max-height: 229px; - overflow: hidden; - margin-top: auto; -} - -@media (max-width:1042px) { - .c1 { - display: inline-block; - margin-right: 16px; - } -} - -
- -
- - - -
-
- Islamabad -
-
-
-
-
- -
-
- 5s ago -
-
-
-
-
- - - -
-
- Berlin -
-
-
-
-
- -
-
- 5m ago -
-
-
-
-
- - - -
-
- st-paul -
-
-
-
-
- -
-
- 5h ago -
-
-
-
-
- - - -
-
- Tokyo -
-
-
-
-
- -
-
- 5d ago -
-
-
-
-
- - - -
-
- New York -
-
-
-
-
- -
-
- 1 Mon ago -
-
-
-
-
- - - -
-
- Toronto -
-
-
-
-
- -
-
- 5 Mon ago -
-
-
-
-
- - - -
-
- Sydney -
-
-
-
-
- -
-
- 5 Yr ago -
-
-
-
-
- - - -
-
- Paris -
-
-
-
-
- -
-
- 5 Yr ago -
-
-
-
-
- -
, - .c0 { - padding-left: 18px; -} - -@media (max-width:1042px) { - -} - -
-
-
-

- 1 Others ... -

-
-
-
, -] -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx deleted file mode 100644 index 916f1cbb63e53..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiHideFor } from '@elastic/eui'; -import { LocationStatusTags } from './location_status_tags'; -import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; -import { MonitorLocations, MonitorLocation } from '../../../../common/runtime_types'; -import { UNNAMED_LOCATION } from '../../../../common/constants'; -import { LocationMissingWarning } from './location_missing'; - -// These height/width values are used to make sure map is in center of panel -// And to make sure, it doesn't take too much space -const MapPanel = styled.div` - height: 240px; - width: 520px; - @media (min-width: 1300px) { - margin-right: 20px; - } - @media (max-width: 574px) { - height: 250px; - width: 100%; - margin-right: 0; - } -`; - -const EuiFlexItemTags = styled(EuiFlexItem)` - padding-top: 5px; - @media (max-width: 1042px) { - flex-basis: 80% !important; - flex-grow: 0 !important; - order: 1; - } -`; - -const FlexGroup = styled(EuiFlexGroup)` - @media (max-width: 850px) { - justify-content: center; - } -`; - -interface LocationMapProps { - monitorLocations: MonitorLocations; -} - -export const LocationMap = ({ monitorLocations }: LocationMapProps) => { - const upPoints: LocationPoint[] = []; - const downPoints: LocationPoint[] = []; - - let isGeoInfoMissing = false; - - if (monitorLocations?.locations) { - monitorLocations.locations.forEach((item: MonitorLocation) => { - if (item.geo?.name === UNNAMED_LOCATION || !item.geo?.location) { - isGeoInfoMissing = true; - } else if ( - item.geo?.name !== UNNAMED_LOCATION && - !!item.geo.location.lat && - !!item.geo.location.lon - ) { - // TypeScript doesn't infer that the above checks in this block's condition - // ensure that lat and lon are defined when we try to pass the location object directly, - // but if we destructure the values it does. Improvement to this block is welcome. - const { lat, lon } = item.geo.location; - if (item?.summary?.down === 0) { - upPoints.push({ lat, lon }); - } else { - downPoints.push({ lat, lon }); - } - } - }); - } - - return ( - - - - - - - - {isGeoInfoMissing && } - - - - - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx deleted file mode 100644 index db84fd5e2ca42..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useContext } from 'react'; -import moment from 'moment'; -import styled from 'styled-components'; -import { EuiBadge, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { UptimeThemeContext } from '../../../contexts'; -import { MonitorLocation } from '../../../../common/runtime_types'; -import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../common/constants'; - -const TimeStampSpan = styled.span` - display: inline-block; - margin-left: 4px; -`; - -const TextStyle = styled.div` - font-weight: 600; -`; - -const BadgeItem = styled.div` - margin-bottom: 5px; - white-space: nowrap; - @media (max-width: 1042px) { - display: inline-block; - margin-right: 16px; - } -`; - -// Set height so that it remains within panel, enough height to display 7 locations tags -const TagContainer = styled.div` - max-height: 229px; - overflow: hidden; - margin-top: auto; -`; - -const OtherLocationsDiv = styled.div` - padding-left: 18px; -`; - -interface Props { - locations: MonitorLocation[]; -} - -interface StatusTag { - label: string; - timestamp: number; -} - -export const LocationStatusTags = ({ locations }: Props) => { - const { - colors: { gray, danger }, - } = useContext(UptimeThemeContext); - - const upLocations: StatusTag[] = []; - const downLocations: StatusTag[] = []; - - locations.forEach((item: any) => { - if (item.summary.down === 0) { - upLocations.push({ label: item.geo.name, timestamp: new Date(item.timestamp).valueOf() }); - } else { - downLocations.push({ label: item.geo.name, timestamp: new Date(item.timestamp).valueOf() }); - } - }); - - // Sort lexicographically by label - upLocations.sort((a, b) => { - return a.label > b.label ? 1 : b.label > a.label ? -1 : 0; - }); - - const tagLabel = (item: StatusTag, ind: number, color: string) => { - return ( - - - - {item.label} - - - - {moment(item.timestamp).fromNow()} - - - ); - }; - - const prevLocal: string = moment.locale() ?? 'en'; - - const renderTags = () => { - const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; - if (!shortLocale) { - moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); - } - - const tags = ( - - {downLocations.map((item, ind) => tagLabel(item, ind, danger))} - {upLocations.map((item, ind) => tagLabel(item, ind, gray))} - - ); - - // Need to reset locale so it doesn't effect other parts of the app - moment.locale(prevLocal); - return tags; - }; - - return ( - <> - {renderTags()} - {locations.length > 7 && ( - - -

- -

-
-
- )} - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap deleted file mode 100644 index ff63b3695fb8d..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorStatusBar component renders duration in ms, not us 1`] = ` -
-
-
-

- Up in 2 Locations -

-
-
- -
- -

- id1 -

-
-
-
-
-
-`; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx deleted file mode 100644 index 73b58e8a33f6b..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { Link } from 'react-router-dom'; -import { EuiSpacer, EuiText, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Tls } from '../../../../../common/runtime_types'; -import { useCertStatus } from '../../../../hooks'; -import { CERT_STATUS, CERTIFICATES_ROUTE } from '../../../../../common/constants'; - -interface Props { - /** - * TLS information coming from monitor in ES heartbeat index - */ - tls: Tls | null | undefined; -} - -export const MonitorSSLCertificate = ({ tls }: Props) => { - const certStatus = useCertStatus(tls?.not_after); - - const isExpiringSoon = certStatus === CERT_STATUS.EXPIRING_SOON; - - const isExpired = certStatus === CERT_STATUS.EXPIRED; - - const relativeDate = moment(tls?.not_after).fromNow(); - - return certStatus ? ( - <> - - {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.title', { - defaultMessage: 'Certificate:', - })} - - - - - - {isExpired ? ( - {relativeDate}, - }} - /> - ) : ( - - {relativeDate} - - ), - }} - /> - )} - - - - - - {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.overview', { - defaultMessage: 'Certificate overview', - })} - - - - - - ) : null; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx deleted file mode 100644 index 36159dc29eccd..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { - EuiLink, - EuiTitle, - EuiTextColor, - EuiSpacer, - EuiText, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitorSSLCertificate } from './ssl_certificate'; -import * as labels from './translations'; -import { StatusByLocations } from './status_by_location'; -import { Ping } from '../../../../../common/runtime_types'; -import { MonitorLocations } from '../../../../../common/runtime_types'; - -interface MonitorStatusBarProps { - monitorId: string; - monitorStatus: Ping | null; - monitorLocations: MonitorLocations; -} - -export const MonitorStatusBarComponent: React.FC = ({ - monitorId, - monitorStatus, - monitorLocations, -}) => { - const full = monitorStatus?.url?.full ?? ''; - - return ( - - - - - - - - {full} - - - - - - -

{monitorId}

-
-
-
- - - - -
- ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts deleted file mode 100644 index f60a1ceeaafb8..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const healthStatusMessageAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', - { - defaultMessage: 'Monitor status', - } -); - -export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { - defaultMessage: 'Up', -}); - -export const downLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', - { - defaultMessage: 'Down', - } -); - -export const monitorUrlLinkAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', - { - defaultMessage: 'Monitor URL link', - } -); - -export const durationTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', - { - defaultMessage: 'Monitor duration in milliseconds', - } -); - -export const timestampFromNowTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', - { - defaultMessage: 'Time since last check', - } -); - -export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { - defaultMessage: 'Loading…', -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap new file mode 100644 index 0000000000000..d53f338b60aed --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorStatusBar component renders 1`] = ` +Array [ +
+
+

+ Up in 2 Locations +

+
+
, +
, + .c0.c0.c0 { + width: 35%; +} + +.c1.c1.c1 { + width: 65%; + overflow-wrap: anywhere; +} + +
+
+ Overall availability +
+
+ 0.00 % +
+
+ Url +
+
+ + +
+ +
+
+ Monitor ID +
+
+
, +] +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap similarity index 61% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap index 628e1d576181e..5b63a09d4f7c4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap @@ -2,61 +2,91 @@ exports[`SSL Certificate component renders 1`] = ` Array [ -
- Certificate: -
, + TLS Certificate + ,
, -
-
-
- Expires - - - - in 2 months - - - -
-
- , + + + , ] `; -exports[`SSL Certificate component renders null if invalid date 1`] = `null`; +exports[`SSL Certificate component renders null if invalid date 1`] = ` +Array [ + .c0.c0.c0 { + width: 35%; +} + +
+ TLS Certificate +
, +
, + .c0.c0.c0 { + width: 65%; + overflow-wrap: anywhere; +} + +
+ + + -- + + +
, +] +`; exports[`SSL Certificate component shallow renders 1`] = ` { let monitorStatus: Ping; @@ -49,18 +50,21 @@ describe('MonitorStatusBar component', () => { const spy = jest.spyOn(redux, 'useDispatch'); spy.mockReturnValue(jest.fn()); - const spy1 = jest.spyOn(redux, 'useSelector'); - spy1.mockReturnValue(true); + jest.spyOn(redux, 'useSelector').mockImplementation((fn, d) => { + if (fn.name === ' monitorStatusSelector') { + return monitorStatus; + } else { + return monitorLocations; + } + }); }); - it('renders duration in ms, not us', () => { - const component = renderWithIntl( - - ); + it('renders', () => { + const history = createMemoryHistory({ + initialEntries: ['/aWQx/'], + }); + history.location.key = 'test'; + const component = renderWithRouter(, history); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx similarity index 75% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx index e8ffc3bf26c42..a4b360ea690d0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; import moment from 'moment'; -import { EuiBadge } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import { Tls } from '../../../../../common/runtime_types'; -import { MonitorSSLCertificate } from '../monitor_status_bar'; +import { MonitorSSLCertificate } from '../status_bar'; import * as redux from 'react-redux'; import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants'; @@ -58,14 +58,12 @@ describe('SSL Certificate component', () => { }; const component = mountWithRouter(); - const badgeComponent = component.find(EuiBadge); + const lockIcon = component.find(EuiIcon); - expect(badgeComponent.props().color).toBe('warning'); + expect(lockIcon.props().color).toBe('warning'); - const badgeComponentText = component.find('.euiBadge__text'); - expect(badgeComponentText.text()).toBe(moment(dateIn5Days).fromNow()); - - expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy(); + const componentText = component.find('h4'); + expect(componentText.text()).toBe('Expires soon ' + moment(dateIn5Days).fromNow()); }); it('does not render the expiration date with a warning state if expiry date is greater than a month', () => { @@ -75,12 +73,10 @@ describe('SSL Certificate component', () => { }; const component = mountWithRouter(); - const badgeComponent = component.find(EuiBadge); - expect(badgeComponent.props().color).toBe('default'); - - const badgeComponentText = component.find('.euiBadge__text'); - expect(badgeComponentText.text()).toBe(moment(dateIn40Days).fromNow()); + const lockIcon = component.find(EuiIcon); + expect(lockIcon.props().color).toBe('success'); - expect(badgeComponent.find('span.euiBadge--warning')).toHaveLength(0); + const componentText = component.find('h4'); + expect(componentText.text()).toBe('Expires ' + moment(dateIn40Days).fromNow()); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/status_by_location.test.tsx similarity index 77% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/__test__/status_by_location.test.tsx index b2619825311d7..b171a8bedb8a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/status_by_location.test.tsx @@ -17,10 +17,16 @@ describe('StatusByLocation component', () => { { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, { summary: { up: 4, down: 0 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = shallowWithIntl(); @@ -32,10 +38,16 @@ describe('StatusByLocation component', () => { { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, { summary: { up: 4, down: 0 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = renderWithIntl(); @@ -47,6 +59,9 @@ describe('StatusByLocation component', () => { { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = renderWithIntl(); @@ -58,6 +73,9 @@ describe('StatusByLocation component', () => { { summary: { up: 0, down: 4 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = renderWithIntl(); @@ -69,10 +87,16 @@ describe('StatusByLocation component', () => { { summary: { up: 0, down: 4 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, { summary: { up: 0, down: 4 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = renderWithIntl(); @@ -84,10 +108,16 @@ describe('StatusByLocation component', () => { { summary: { up: 0, down: 4 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, { summary: { up: 4, down: 0 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, + up_history: 4, + down_history: 0, + timestamp: '2020-01-13T22:50:06.536Z', }, ]; const component = renderWithIntl(); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap new file mode 100644 index 0000000000000..9496274a69171 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap @@ -0,0 +1,381 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AvailabilityReporting component renders correctly against snapshot 1`] = ` +Array [ + @media (max-width:1042px) { + +} + +
, + .c0 { + white-space: nowrap; + display: inline-block; +} + +@media (max-width:1042px) { + .c0 { + display: inline-block; + margin-right: 16px; + } +} + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Location + +
+
+
+ + Availability + +
+
+
+ + Last check + +
+
+
+ Location +
+
+
+ + + +
+

+ au-heartbeat +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 36m ago + +
+
+
+ Location +
+
+
+ + + +
+

+ nyc-heartbeat +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 36m ago + +
+
+
+ Location +
+
+
+ + + +
+

+ spa-heartbeat +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 36m ago + +
+
+
+
, +] +`; + +exports[`AvailabilityReporting component shallow renders correctly against snapshot 1`] = ` + + + + +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap new file mode 100644 index 0000000000000..05e0b50a86f35 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap @@ -0,0 +1,1085 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationStatusTags component renders properly against props 1`] = ` + + + + + +`; + +exports[`LocationStatusTags component renders when all locations are down 1`] = ` +.c1 { + white-space: nowrap; + display: inline-block; +} + +.c0 { + max-height: 246px; + overflow: hidden; +} + +@media (max-width:1042px) { + .c1 { + display: inline-block; + margin-right: 16px; + } +} + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Location + +
+
+
+ + Availability + +
+
+
+ + Last check + +
+
+
+ Location +
+
+
+ + + +
+

+ Berlin +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5m ago + +
+
+
+ Location +
+
+
+ + + +
+

+ Islamabad +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5s ago + +
+
+
+
+
+`; + +exports[`LocationStatusTags component renders when all locations are up 1`] = ` +.c1 { + white-space: nowrap; + display: inline-block; +} + +.c0 { + max-height: 246px; + overflow: hidden; +} + +@media (max-width:1042px) { + .c1 { + display: inline-block; + margin-right: 16px; + } +} + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Location + +
+
+
+ + Availability + +
+
+
+ + Last check + +
+
+
+ Location +
+
+
+ + + +
+

+ Berlin +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5d ago + +
+
+
+ Location +
+
+
+ + + +
+

+ Islamabad +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5s ago + +
+
+
+
+
+`; + +exports[`LocationStatusTags component renders when there are many location 1`] = ` +.c1 { + white-space: nowrap; + display: inline-block; +} + +.c0 { + max-height: 246px; + overflow: hidden; +} + +@media (max-width:1042px) { + .c1 { + display: inline-block; + margin-right: 16px; + } +} + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Location + +
+
+
+ + Availability + +
+
+
+ + Last check + +
+
+
+ Location +
+
+
+ + + +
+

+ Berlin +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5m ago + +
+
+
+ Location +
+
+
+ + + +
+

+ Islamabad +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5s ago + +
+
+
+ Location +
+
+
+ + + +
+

+ New York +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 1 Mon ago + +
+
+
+ Location +
+
+
+ + + +
+

+ Paris +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5 Yr ago + +
+
+
+ Location +
+
+
+ + + +
+

+ Sydney +

+
+
+
+
+
+
+
+
+ Availability +
+
+ + 100.00 % + +
+
+
+ Last check +
+
+ + 5 Yr ago + +
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap new file mode 100644 index 0000000000000..3381efa62286b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagLabel component renders correctly against snapshot 1`] = ` +.c0 { + white-space: nowrap; + display: inline-block; +} + +@media (max-width:1042px) { + .c0 { + display: inline-block; + margin-right: 16px; + } +} + +
+ + + +
+

+ US-East +

+
+
+
+
+
+`; + +exports[`TagLabel component shallow render correctly against snapshot 1`] = ` + + + +

+ US-East +

+
+
+
+`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx new file mode 100644 index 0000000000000..de9f6b0d3b30f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { AvailabilityReporting } from '../availability_reporting'; +import { StatusTag } from '../location_status_tags'; + +describe('AvailabilityReporting component', () => { + let allLocations: StatusTag[]; + + beforeEach(() => { + allLocations = [ + { + label: 'au-heartbeat', + timestamp: '36m ago', + color: '#d3dae6', + availability: 100, + }, + { + label: 'nyc-heartbeat', + timestamp: '36m ago', + color: '#d3dae6', + availability: 100, + }, + { label: 'spa-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100 }, + ]; + }); + + it('shallow renders correctly against snapshot', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders correctly against snapshot', () => { + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx similarity index 84% rename from x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx index 28b4482401793..bfeaa6085e998 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { MonitorLocation } from '../../../../../common/runtime_types/monitor'; +import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; import { LocationStatusTags } from '../index'; describe('LocationStatusTags component', () => { @@ -19,16 +19,22 @@ describe('LocationStatusTags component', () => { summary: { up: 4, down: 0 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'w').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'w').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'w').toISOString(), + up_history: 4, + down_history: 0, }, ]; const component = shallowWithIntl(); @@ -41,41 +47,57 @@ describe('LocationStatusTags component', () => { summary: { up: 0, down: 1 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 's').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'm').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'h').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'd').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'New York', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'w').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Toronto', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'M').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Sydney', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'y').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Paris', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'y').toISOString(), + up_history: 4, + down_history: 0, }, ]; const component = renderWithIntl(); @@ -88,11 +110,15 @@ describe('LocationStatusTags component', () => { summary: { up: 4, down: 0 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 's').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'd').toISOString(), + up_history: 4, + down_history: 0, }, ]; const component = renderWithIntl(); @@ -105,11 +131,15 @@ describe('LocationStatusTags component', () => { summary: { up: 0, down: 2 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 's').toISOString(), + up_history: 4, + down_history: 0, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: moment().subtract('5', 'm').toISOString(), + up_history: 4, + down_history: 0, }, ]; const component = renderWithIntl(); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx new file mode 100644 index 0000000000000..3560784122298 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { TagLabel } from '../tag_label'; + +describe('TagLabel component', () => { + it('shallow render correctly against snapshot', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders correctly against snapshot', () => { + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx new file mode 100644 index 0000000000000..8fed5db5e0271 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Pagination } from '@elastic/eui/src/components/basic_table/pagination_bar'; +import { StatusTag } from './location_status_tags'; +import { TagLabel } from './tag_label'; +import { AvailabilityLabel, LastCheckLabel, LocationLabel } from '../translations'; + +interface Props { + allLocations: StatusTag[]; +} + +export const formatAvailabilityValue = (val: number) => { + const result = Math.round(val * 100) / 100; + return result.toFixed(2); +}; + +export const AvailabilityReporting: React.FC = ({ allLocations }) => { + const [pageIndex, setPageIndex] = useState(0); + + const cols = [ + { + field: 'label', + name: LocationLabel, + truncateText: true, + render: (val: string, item: StatusTag) => { + return ; + }, + }, + { + field: 'availability', + name: AvailabilityLabel, + align: 'right' as const, + render: (val: number) => { + return ( + + + + ); + }, + }, + { + name: LastCheckLabel, + field: 'timestamp', + align: 'right' as const, + }, + ]; + const pageSize = 5; + + const pagination: Pagination = { + pageIndex, + pageSize, + totalItemCount: allLocations.length, + hidePerPageOptions: true, + }; + + const onTableChange = ({ page }: any) => { + setPageIndex(page.index); + }; + + const paginationProps = allLocations.length > pageSize ? { pagination } : {}; + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/index.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/index.ts new file mode 100644 index 0000000000000..fb42a162522a8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AvailabilityReporting } from './availability_reporting'; +export { LocationStatusTags } from './location_status_tags'; +export { TagLabel } from './tag_label'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx new file mode 100644 index 0000000000000..6096499213a10 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import moment from 'moment'; +import styled from 'styled-components'; +import { UptimeThemeContext } from '../../../../contexts'; +import { MonitorLocation } from '../../../../../common/runtime_types'; +import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../../common/constants'; +import { AvailabilityReporting } from '../index'; + +// Set height so that it remains within panel, enough height to display 7 locations tags +const TagContainer = styled.div` + max-height: 246px; + overflow: hidden; +`; + +interface Props { + locations: MonitorLocation[]; +} + +export interface StatusTag { + label: string; + timestamp: string; + color: string; + availability: number; +} + +export const LocationStatusTags = ({ locations }: Props) => { + const { + colors: { gray, danger }, + } = useContext(UptimeThemeContext); + + const allLocations: StatusTag[] = []; + const prevLocal: string = moment.locale() ?? 'en'; + + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + + locations.forEach((item: MonitorLocation) => { + allLocations.push({ + label: item.geo.name!, + timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(), + color: item.summary.down === 0 ? gray : danger, + availability: (item.up_history / (item.up_history + item.down_history)) * 100, + }); + }); + + // Need to reset locale so it doesn't effect other parts of the app + moment.locale(prevLocal); + + // Sort lexicographically by label + allLocations.sort((a, b) => { + return a.label > b.label ? 1 : b.label > a.label ? -1 : 0; + }); + + if (allLocations.length === 0) { + return null; + } + + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx new file mode 100644 index 0000000000000..dbd73fc7d440b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiText } from '@elastic/eui'; + +const BadgeItem = styled.div` + white-space: nowrap; + display: inline-block; + @media (max-width: 1042px) { + display: inline-block; + margin-right: 16px; + } +`; + +interface Props { + color: string; + label: string; +} + +export const TagLabel: React.FC = ({ color, label }) => { + return ( + + + +

{label}

+
+
+
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/index.ts similarity index 63% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/index.ts index e95f14472e9e8..ae3a07d231da3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MonitorStatusBarComponent } from './monitor_status_bar'; export { MonitorStatusDetailsComponent } from './status_details'; -export { StatusByLocations } from './monitor_status_bar/status_by_location'; +export { StatusByLocations } from './status_bar/status_by_location'; export { MonitorStatusDetails } from './status_details_container'; -export { MonitorStatusBar } from './monitor_status_bar/status_bar_container'; +export { MonitorStatusBar } from './status_bar/status_bar'; +export { AvailabilityReporting } from './availability_reporting/availability_reporting'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/__snapshots__/location_availability.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/__snapshots__/location_availability.test.tsx.snap new file mode 100644 index 0000000000000..94cbeb49a32cf --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/__snapshots__/location_availability.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationAvailability component doesnt shows warning if geo is provided 1`] = ` + + + + + + + + + + + + + +`; + +exports[`LocationAvailability component renders correctly against snapshot 1`] = ` + + + + +

+ Monitoring from +

+
+
+ + + +
+ + + + + +
+`; + +exports[`LocationAvailability component renders named locations that have missing geo data 1`] = ` + + + + + + + + + + + + + + + +`; + +exports[`LocationAvailability component shows warning if geo information is missing 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/location_availability.test.tsx similarity index 64% rename from x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/location_availability.test.tsx index 1913e677dd674..d00cb1f24def8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__tests__/location_availability.test.tsx @@ -6,59 +6,85 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LocationMap } from '../location_map'; -import { MonitorLocations } from '../../../../../common/runtime_types'; -import { LocationMissingWarning } from '../location_missing'; +import { LocationAvailability } from '../location_availability'; +import { MonitorLocations } from '../../../../../../common/runtime_types'; +import { LocationMissingWarning } from '../../location_map/location_missing'; // Note For shallow test, we need absolute time strings -describe('LocationMap component', () => { +describe('LocationAvailability component', () => { let monitorLocations: MonitorLocations; + let localStorageMock: any; + + let selectedView = 'list'; beforeEach(() => { + localStorageMock = { + getItem: jest.fn().mockImplementation(() => selectedView), + setItem: jest.fn(), + }; + + // @ts-ignore replacing a call to localStorage we use for monitor list size + global.localStorage = localStorageMock; + monitorLocations = { monitorId: 'wapo', + up_history: 12, + down_history: 0, locations: [ { summary: { up: 4, down: 0 }, geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } }, timestamp: '2020-01-13T22:50:06.536Z', + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Unnamed-location' }, timestamp: '2020-01-13T22:50:02.753Z', + up_history: 4, + down_history: 0, }, ], }; }); it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('shows warning if geo information is missing', () => { + selectedView = 'map'; monitorLocations = { monitorId: 'wapo', + up_history: 8, + down_history: 0, locations: [ { summary: { up: 4, down: 0 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Unnamed-location' }, timestamp: '2020-01-13T22:50:02.753Z', + up_history: 4, + down_history: 0, }, ], }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); const warningComponent = component.find(LocationMissingWarning); @@ -68,20 +94,26 @@ describe('LocationMap component', () => { it('doesnt shows warning if geo is provided', () => { monitorLocations = { monitorId: 'wapo', + up_history: 8, + down_history: 0, locations: [ { summary: { up: 4, down: 0 }, geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } }, timestamp: '2020-01-13T22:50:06.536Z', + up_history: 4, + down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', + up_history: 4, + down_history: 0, }, ], }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); const warningComponent = component.find(LocationMissingWarning); @@ -91,16 +123,20 @@ describe('LocationMap component', () => { it('renders named locations that have missing geo data', () => { monitorLocations = { monitorId: 'wapo', + up_history: 4, + down_history: 0, locations: [ { summary: { up: 4, down: 0 }, geo: { name: 'New York', location: undefined }, timestamp: '2020-01-13T22:50:06.536Z', + up_history: 4, + down_history: 0, }, ], }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx new file mode 100644 index 0000000000000..89b969fdcf691 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiTitle } from '@elastic/eui'; +import { LocationStatusTags } from '../availability_reporting'; +import { LocationPoint } from '../location_map/embeddables/embedded_map'; +import { MonitorLocations, MonitorLocation } from '../../../../../common/runtime_types'; +import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { LocationMissingWarning } from '../location_map/location_missing'; +import { useSelectedView } from './use_selected_view'; +import { LocationMap } from '../location_map'; +import { MonitoringFrom } from '../translations'; +import { ToggleViewBtn } from './toggle_view_btn'; + +const EuiFlexItemTags = styled(EuiFlexItem)` + width: 350px; + @media (max-width: 1042px) { + width: 100%; + } +`; + +interface LocationMapProps { + monitorLocations: MonitorLocations; +} + +export const LocationAvailability = ({ monitorLocations }: LocationMapProps) => { + const upPoints: LocationPoint[] = []; + const downPoints: LocationPoint[] = []; + + let isAnyGeoInfoMissing = false; + + if (monitorLocations?.locations) { + monitorLocations.locations.forEach(({ geo, summary }: MonitorLocation) => { + if (geo?.name === UNNAMED_LOCATION || !geo?.location) { + isAnyGeoInfoMissing = true; + } else if (!!geo.location.lat && !!geo.location.lon) { + if (summary?.down === 0) { + upPoints.push(geo as LocationPoint); + } else { + downPoints.push(geo as LocationPoint); + } + } + }); + } + const { selectedView: initialView } = useSelectedView(); + + const [selectedView, setSelectedView] = useState(initialView); + + return ( + + + {selectedView === 'list' && ( + + +

{MonitoringFrom}

+
+
+ )} + {selectedView === 'map' && ( + {isAnyGeoInfoMissing && } + )} + + { + setSelectedView(val); + }} + /> + +
+ + + {selectedView === 'list' && ( + + + + )} + {selectedView === 'map' && ( + + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx new file mode 100644 index 0000000000000..dfce583945cce --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import styled from 'styled-components'; +import { EuiButtonGroup } from '@elastic/eui'; +import { useSelectedView } from './use_selected_view'; +import { ChangeToListView, ChangeToMapView } from '../translations'; + +const ToggleViewButtons = styled.span` + margin-left: auto; +`; + +interface Props { + onChange: (val: string) => void; +} + +export const ToggleViewBtn = ({ onChange }: Props) => { + const toggleButtons = [ + { + id: `listBtn`, + label: ChangeToMapView, + name: 'listView', + iconType: 'list', + 'data-test-subj': 'uptimeMonitorToggleListBtn', + 'aria-label': ChangeToMapView, + }, + { + id: `mapBtn`, + label: ChangeToListView, + name: 'mapView', + iconType: 'mapMarker', + 'data-test-subj': 'uptimeMonitorToggleMapBtn', + 'aria-label': ChangeToListView, + }, + ]; + + const { selectedView, setSelectedView } = useSelectedView(); + + const onChangeView = (optionId: string) => { + const currView = optionId === 'listBtn' ? 'list' : 'map'; + setSelectedView(currView); + onChange(currView); + }; + + return ( + + onChangeView(id)} + type="multi" + isIconOnly + style={{ marginLeft: 'auto' }} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts new file mode 100644 index 0000000000000..f132f502666f0 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +const localKey = 'xpack.uptime.detailPage.selectedView'; + +interface Props { + selectedView: string; + setSelectedView: (val: string) => void; +} + +export const useSelectedView = (): Props => { + const getSelectedView = localStorage.getItem(localKey) ?? 'list'; + + const [selectedView, setSelectedView] = useState(getSelectedView); + + useEffect(() => { + localStorage.setItem(localKey, selectedView); + }, [selectedView]); + + return { selectedView, setSelectedView }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_map.test.tsx.snap new file mode 100644 index 0000000000000..6b3d157c23fee --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_map.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationMap component renders correctly against snapshot 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap similarity index 99% rename from x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap index 150c4581dfd13..cd282f916f46c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap @@ -4,6 +4,7 @@ exports[`LocationMissingWarning component renders correctly against snapshot 1`] .c0 { margin-left: auto; margin-bottom: 3px; + margin-right: 5px; }
{ + let upPoints: LocationPoint[]; + + beforeEach(() => { + upPoints = [ + { + name: 'New York', + location: { lat: '40.730610', lon: ' -73.935242' }, + }, + { + name: 'Tokyo', + location: { lat: '52.487448', lon: ' 13.394798' }, + }, + ]; + }); + + it('renders correctly against snapshot', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/location_missing.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__tests__/location_missing.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 96% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/mock.ts index 291ab555fbdc6..626011e3d09c9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/mock.ts @@ -14,6 +14,7 @@ export const mockDownPointsLayer = { __featureCollection: { features: [ { + id: 'Asia', type: 'feature', geometry: { type: 'Point', @@ -21,6 +22,7 @@ export const mockDownPointsLayer = { }, }, { + id: 'APJ', type: 'feature', geometry: { type: 'Point', @@ -28,6 +30,7 @@ export const mockDownPointsLayer = { }, }, { + id: 'Canada', type: 'feature', geometry: { type: 'Point', @@ -79,6 +82,7 @@ export const mockUpPointsLayer = { __featureCollection: { features: [ { + id: 'US-EAST', type: 'feature', geometry: { type: 'Point', @@ -86,6 +90,7 @@ export const mockUpPointsLayer = { }, }, { + id: 'US-WEST', type: 'feature', geometry: { type: 'Point', @@ -93,6 +98,7 @@ export const mockUpPointsLayer = { }, }, { + id: 'Europe', type: 'feature', geometry: { type: 'Point', diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts similarity index 65% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts index 7d53d784ff338..09a41bd9eb4b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts @@ -7,7 +7,7 @@ import { getLayerList } from '../map_config'; import { mockLayerList } from './__mocks__/mock'; import { LocationPoint } from '../embedded_map'; -import { UptimeAppColors } from '../../../../../uptime_app'; +import { UptimeAppColors } from '../../../../../../uptime_app'; jest.mock('uuid', () => { return { @@ -22,14 +22,14 @@ describe('map_config', () => { beforeEach(() => { upPoints = [ - { lat: '52.487239', lon: '13.399262' }, - { lat: '55.487239', lon: '13.399262' }, - { lat: '54.487239', lon: '14.399262' }, + { name: 'US-EAST', location: { lat: '52.487239', lon: '13.399262' } }, + { location: { lat: '55.487239', lon: '13.399262' }, name: 'US-WEST' }, + { location: { lat: '54.487239', lon: '14.399262' }, name: 'Europe' }, ]; downPoints = [ - { lat: '52.487239', lon: '13.399262' }, - { lat: '55.487239', lon: '13.399262' }, - { lat: '54.487239', lon: '14.399262' }, + { location: { lat: '52.487239', lon: '13.399262' }, name: 'Asia' }, + { location: { lat: '55.487239', lon: '13.399262' }, name: 'APJ' }, + { location: { lat: '54.487239', lon: '14.399262' }, name: 'Canada' }, ]; colors = { danger: '#BC261E', diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx similarity index 68% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx index 06cdb07bd8bcd..648418c02489a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx @@ -7,24 +7,30 @@ import React, { useEffect, useState, useContext, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; -import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../../legacy/plugins/maps/public'; +import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; +import { + MapEmbeddable, + MapEmbeddableInput, +} from '../../../../../../../../legacy/plugins/maps/public'; import * as i18n from './translations'; -import { Location } from '../../../../../common/runtime_types'; +import { GeoPoint } from '../../../../../../common/runtime_types'; import { getLayerList } from './map_config'; -import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../contexts'; +import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../../contexts'; import { isErrorEmbeddable, ViewMode, ErrorEmbeddable, -} from '../../../../../../../../src/plugins/embeddable/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/public'; +} from '../../../../../../../../../src/plugins/embeddable/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; +import { MapToolTipComponent } from './map_tool_tip'; +import { RenderTooltipContentParams } from '../../../../../../../../legacy/plugins/maps/public'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; downPoints: LocationPoint[]; } -export type LocationPoint = Required; +export type LocationPoint = Required; const EmbeddedPanel = styled.div` z-index: auto; @@ -54,6 +60,8 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp } const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + const portalNode = React.useMemo(() => createPortalNode(), []); + const input: MapEmbeddableInput = { id: uuid.v4(), filters: [], @@ -73,12 +81,38 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp zoom: 0, }, disableInteractive: true, - disableTooltipControl: true, hideToolbarOverlay: true, hideLayerControl: true, hideViewControl: true, }; + const renderTooltipContent = ({ + addFilters, + closeTooltip, + features, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }: RenderTooltipContentParams) => { + const props = { + addFilters, + closeTooltip, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }; + const relevantFeatures = features.filter( + (item: any) => item.layerId === 'up_points' || item.layerId === 'down_points' + ); + if (relevantFeatures.length > 0) { + return ; + } + closeTooltip(); + return null; + }; + useEffect(() => { async function setupEmbeddable() { if (!factory) { @@ -90,11 +124,13 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp }); if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); + embeddableObject.setRenderTooltipContent(renderTooltipContent); + await embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); } setEmbeddable(embeddableObject); } + setupEmbeddable(); // we want this effect to execute exactly once after the component mounts @@ -122,6 +158,9 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp className="embPanel__content" ref={embeddableRoot} /> + + + ); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts similarity index 94% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts index f1b530c767f1f..e766641102a24 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts @@ -6,7 +6,7 @@ import lowPolyLayerFeatures from './low_poly_layer.json'; import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../uptime_app'; +import { UptimeAppColors } from '../../../../../uptime_app'; /** * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, @@ -70,9 +70,10 @@ export const getLowPolyLayer = () => { export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => { const features = downPoints?.map((point) => ({ type: 'feature', + id: point.name, geometry: { type: 'Point', - coordinates: [+point.lon, +point.lat], + coordinates: [+point.location.lon, +point.location.lat], }, })); return { @@ -122,9 +123,10 @@ export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: str export const getUpPointsLayer = (upPoints: LocationPoint[]) => { const features = upPoints?.map((point) => ({ type: 'feature', + id: point.name, geometry: { type: 'Point', - coordinates: [+point.lon, +point.lat], + coordinates: [+point.location.lon, +point.location.lat], }, })); return { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx new file mode 100644 index 0000000000000..0d54d91007a8d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiOutsideClickDetector, + EuiPopoverTitle, +} from '@elastic/eui'; +import { TagLabel } from '../../availability_reporting'; +import { UptimeThemeContext } from '../../../../../contexts'; +import { AppState } from '../../../../../state'; +import { monitorLocationsSelector } from '../../../../../state/selectors'; +import { useMonitorId } from '../../../../../hooks'; +import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; +import { RenderTooltipContentParams } from '../../../../../../../../legacy/plugins/maps/public'; +import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; +import { LastCheckLabel } from '../../translations'; + +type MapToolTipProps = Partial; + +export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipProps) => { + const { id: featureId, layerId } = features[0] ?? {}; + const locationName = featureId?.toString(); + const { + colors: { gray, danger }, + } = useContext(UptimeThemeContext); + + const monitorId = useMonitorId(); + + const monitorLocations = useSelector((state: AppState) => + monitorLocationsSelector(state, monitorId) + ); + if (!locationName || !monitorLocations?.locations) { + return null; + } + const { + timestamp, + up_history: ups, + down_history: downs, + }: MonitorLocation = monitorLocations.locations!.find( + ({ geo }: MonitorLocation) => geo.name === locationName + )!; + + const availability = (ups / (ups + downs)) * 100; + + return ( + { + if (closeTooltip != null) { + closeTooltip(); + } + }} + > + <> + + {layerId === 'up_points' ? ( + + ) : ( + + )} + + + Availability + + {i18n.translate('xpack.uptime.mapToolTip.AvailabilityStat.title', { + defaultMessage: '{value} %', + values: { value: formatAvailabilityValue(availability) }, + description: 'A percentage value like 23.5%', + })} + + {LastCheckLabel} + + {moment(timestamp).fromNow()} + + + + + ); +}; + +export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/index.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts similarity index 81% rename from x-pack/plugins/uptime/public/components/monitor/location_map/index.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts index 140d33bbeef66..4261400bebc6f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts @@ -5,4 +5,4 @@ */ export * from './location_map'; -export * from './location_status_tags'; +export * from '../availability_reporting/location_status_tags'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx new file mode 100644 index 0000000000000..3d0a097d20d70 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; + +// These height/width values are used to make sure map is in center of panel +// And to make sure, it doesn't take too much space +const MapPanel = styled.div` + height: 240px; + width: 520px; + margin-right: 65px; + @media (max-width: 574px) { + height: 250px; + width: 100%; + } +`; + +interface Props { + upPoints: LocationPoint[]; + downPoints: LocationPoint[]; +} + +export const LocationMap = ({ upPoints, downPoints }: Props) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx similarity index 96% rename from x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx index 6ce31e4cc8243..e364b6b8940b3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx @@ -16,11 +16,12 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LocationLink } from '../../common/location_link'; +import { LocationLink } from '../../../common/location_link'; const EuiPopoverRight = styled(EuiFlexItem)` margin-left: auto; margin-bottom: 3px; + margin-right: 5px; `; export const LocationMissingWarning = () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/index.ts similarity index 72% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/index.ts index 3c861412a39e9..22a059d603778 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/index.ts @@ -5,5 +5,4 @@ */ export { MonitorSSLCertificate } from './ssl_certificate'; -export { MonitorStatusBarComponent } from './status_bar'; -export { MonitorStatusBar } from './status_bar_container'; +export { MonitorStatusBar } from './status_bar'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx new file mode 100644 index 0000000000000..93720ab313ee3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Tls } from '../../../../../common/runtime_types'; +import { CERTIFICATES_ROUTE } from '../../../../../common/constants'; +import { MonListDescription, MonListTitle } from './status_bar'; +import { CertStatusColumn } from '../../../overview/monitor_list/cert_status_column'; + +interface Props { + /** + * TLS information coming from monitor in ES heartbeat index + */ + tls: Tls | undefined; +} + +export const MonitorSSLCertificate = ({ tls }: Props) => { + return tls?.not_after ? ( + <> + + + + + + + + + + + + ) : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx new file mode 100644 index 0000000000000..afcc8fae7a8ac --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { + EuiLink, + EuiIcon, + EuiSpacer, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MonitorSSLCertificate } from './ssl_certificate'; +import * as labels from '../translations'; +import { StatusByLocations } from './status_by_location'; +import { useStatusBar } from './use_status_bar'; +import { MonitorIDLabel, OverallAvailability } from '../translations'; +import { URL_LABEL } from '../../../common/translations'; +import { MonitorLocations } from '../../../../../common/runtime_types/monitor'; +import { formatAvailabilityValue } from '../availability_reporting/availability_reporting'; + +export const MonListTitle = styled(EuiDescriptionListTitle)` + &&& { + width: 35%; + } +`; + +export const MonListDescription = styled(EuiDescriptionListDescription)` + &&& { + width: 65%; + overflow-wrap: anywhere; + } +`; + +export const MonitorStatusBar: React.FC = () => { + const { monitorId, monitorStatus, monitorLocations = {} } = useStatusBar(); + + const { locations, up_history: ups, down_history: downs } = monitorLocations as MonitorLocations; + + const full = monitorStatus?.url?.full ?? ''; + + const availability = (ups === 0 && downs === 0) || !ups ? 0 : (ups / (ups + downs)) * 100; + + return ( + <> +
+ +
+ + + {OverallAvailability} + + + + {URL_LABEL} + + + {full} + + + {MonitorIDLabel} + {monitorId} + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/use_status_bar.ts similarity index 66% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/use_status_bar.ts index 9562295437515..27e953aab1b71 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/use_status_bar.ts @@ -4,24 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { monitorLocationsSelector, monitorStatusSelector } from '../../../../state/selectors'; -import { MonitorStatusBarComponent } from './index'; -import { getMonitorStatusAction } from '../../../../state/actions'; -import { useGetUrlParams } from '../../../../hooks'; import { UptimeRefreshContext } from '../../../../contexts'; -import { MonitorIdParam } from '../../../../../common/types'; +import { useGetUrlParams, useMonitorId } from '../../../../hooks'; +import { monitorLocationsSelector, monitorStatusSelector } from '../../../../state/selectors'; import { AppState } from '../../../../state'; +import { getMonitorStatusAction } from '../../../../state/actions'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { MonitorLocations } from '../../../../../common/runtime_types/monitor'; -export const MonitorStatusBar: React.FC = ({ monitorId }) => { +interface MonitorStatusBarProps { + monitorId: string; + monitorStatus: Ping | null; + monitorLocations?: MonitorLocations; +} + +export const useStatusBar = (): MonitorStatusBarProps => { const { lastRefresh } = useContext(UptimeRefreshContext); const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); const dispatch = useDispatch(); + const monitorId = useMonitorId(); + const monitorStatus = useSelector(monitorStatusSelector); + const monitorLocations = useSelector((state: AppState) => monitorLocationsSelector(state, monitorId) ); @@ -30,11 +39,5 @@ export const MonitorStatusBar: React.FC = ({ monitorId }) => { dispatch(getMonitorStatusAction({ dateStart, dateEnd, monitorId })); }, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]); - return ( - - ); + return { monitorStatus, monitorLocations, monitorId }; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx similarity index 72% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx index 3766ac86785f7..dea39494646a6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx @@ -7,31 +7,24 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; -import { LocationMap } from '../location_map'; +import { LocationAvailability } from './location_availability/location_availability'; import { UptimeRefreshContext } from '../../../contexts'; import { MonitorLocations } from '../../../../common/runtime_types'; -import { MonitorStatusBar } from './monitor_status_bar'; +import { MonitorStatusBar } from './status_bar'; interface MonitorStatusDetailsProps { - monitorId: string; monitorLocations: MonitorLocations; } const WrapFlexItem = styled(EuiFlexItem)` &&& { - @media (max-width: 768px) { - width: 100%; - } - @media (max-width: 1042px) { - flex-basis: 520px; + @media (max-width: 800px) { + flex-basis: 100%; } } `; -export const MonitorStatusDetailsComponent = ({ - monitorId, - monitorLocations, -}: MonitorStatusDetailsProps) => { +export const MonitorStatusDetailsComponent = ({ monitorLocations }: MonitorStatusDetailsProps) => { const { refreshApp } = useContext(UptimeRefreshContext); const [isTabActive] = useState(document.visibilityState); @@ -56,12 +49,12 @@ export const MonitorStatusDetailsComponent = ({ return ( - - - + + + - - + + diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details_container.tsx similarity index 92% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/status_details/status_details_container.tsx index 251f3562f9d1a..92e144bd1b9cc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details_container.tsx @@ -28,7 +28,5 @@ export const MonitorStatusDetails: React.FC = ({ monitorId }) => dispatch(getMonitorLocationsAction({ dateStart, dateEnd, monitorId })); }, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]); - return ( - - ); + return ; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts similarity index 50% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts index f60a1ceeaafb8..f593525fa0942 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts @@ -48,3 +48,56 @@ export const timestampFromNowTextAriaLabel = i18n.translate( export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { defaultMessage: 'Loading…', }); + +export const MonitorIDLabel = i18n.translate('xpack.uptime.monitorStatusBar.monitor.id', { + defaultMessage: 'Monitor ID', +}); + +export const OverallAvailability = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.availability', + { + defaultMessage: 'Overall availability', + } +); + +export const MonitoringFrom = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.monitoringFrom', + { + defaultMessage: 'Monitoring from', + } +); + +export const ChangeToMapView = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.monitoringFrom.listToMap', + { + defaultMessage: 'Change to map view to check availability by location.', + } +); + +export const ChangeToListView = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.monitoringFrom.MapToList', + { + defaultMessage: 'Change to list view to check availability by location.', + } +); + +export const LocationLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.availabilityReport.location', + { + defaultMessage: 'Location', + } +); + +export const AvailabilityLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.availabilityReport.availability', + { + defaultMessage: 'Availability', + } +); + +export const LastCheckLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitor.availabilityReport.lastCheck', + { + defaultMessage: 'Last check', + } +); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx index 5ad0f4d3d1d49..b7c70198912fc 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx @@ -8,13 +8,14 @@ import React from 'react'; import moment from 'moment'; import styled from 'styled-components'; import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; -import { Cert } from '../../../../common/runtime_types'; +import { Cert, Tls } from '../../../../common/runtime_types'; import { useCertStatus } from '../../../hooks'; -import { EXPIRED, EXPIRES_SOON } from '../../certificates/translations'; +import { EXPIRED, EXPIRES, EXPIRES_SOON } from '../../certificates/translations'; import { CERT_STATUS } from '../../../../common/constants'; interface Props { - cert: Cert; + cert: Cert | Tls; + boldStyle?: boolean; } const Span = styled.span` @@ -22,7 +23,15 @@ const Span = styled.span` vertical-align: middle; `; -export const CertStatusColumn: React.FC = ({ cert }) => { +const H4Text = styled.h4` + &&& { + margin: 0 0 0 4px; + display: inline-block; + color: inherit; + } +`; + +export const CertStatusColumn: React.FC = ({ cert, boldStyle = false }) => { const certStatus = useCertStatus(cert?.not_after); const relativeDate = moment(cert?.not_after).fromNow(); @@ -32,9 +41,15 @@ export const CertStatusColumn: React.FC = ({ cert }) => { - - {text} {relativeDate} - + {boldStyle ? ( + + {text} {relativeDate} + + ) : ( + + {text} {relativeDate} + + )} ); @@ -47,5 +62,5 @@ export const CertStatusColumn: React.FC = ({ cert }) => { return ; } - return certStatus ? : --; + return certStatus ? : --; }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 1526838460957..75d587579f66f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -30,6 +30,7 @@ import { MonitorListProps } from './monitor_list_container'; import { MonitorList } from '../../../state/reducers/monitor_list'; import { CertStatusColumn } from './cert_status_column'; import { MonitorListHeader } from './monitor_list_header'; +import { URL_LABEL } from '../../common/translations'; interface Props extends MonitorListProps { pageSize: number; @@ -106,7 +107,7 @@ export const MonitorListComponent: React.FC = ({ { align: 'left' as const, field: 'state.url.full', - name: labels.URL, + name: URL_LABEL, render: (url: string, summary: MonitorSummary) => ( {url} diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts index e70eef1d91161..ee922a9ef803f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts @@ -62,10 +62,6 @@ export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMe description: 'This message is shown if the monitors table is rendered but has no items.', }); -export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { - defaultMessage: 'Url', -}); - export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { defaultMessage: 'Up', }); diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap index ec0ee120b6283..dbdfd6b27e69f 100644 --- a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap @@ -2,21 +2,25 @@ exports[`PageHeader shallow renders extra links: page_header_with_extra_links 1`] = ` Array [ - @media only screen and (max-width:1024px) and (min-width:868px) { - .c0.c0.c0 .euiSuperDatePicker__flexWrapper { + .c0 { + white-space: nowrap; +} + +@media only screen and (max-width:1024px) and (min-width:868px) { + .c1.c1.c1 .euiSuperDatePicker__flexWrapper { width: 500px; } } @media only screen and (max-width:880px) { - .c0.c0.c0 { + .c1.c1.c1 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } - .c0.c0.c0 .euiSuperDatePicker__flexWrapper { + .c1.c1.c1 .euiSuperDatePicker__flexWrapper { width: calc(100% + 8px); } } @@ -28,7 +32,7 @@ Array [ class="euiFlexItem" >

TestingHeading

@@ -103,7 +107,7 @@ Array [

TestingHeading

@@ -250,7 +258,7 @@ Array [ class="euiFlexItem euiFlexItem--flexGrowZero" />

TestingHeading

diff --git a/x-pack/plugins/uptime/public/pages/page_header.tsx b/x-pack/plugins/uptime/public/pages/page_header.tsx index d2e1f9036a24a..3b228afe904f4 100644 --- a/x-pack/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/plugins/uptime/public/pages/page_header.tsx @@ -18,6 +18,7 @@ interface PageHeaderProps { extraLinks?: boolean; datePicker?: boolean; } + const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.settingsLink', { defaultMessage: 'Settings', }); @@ -38,6 +39,10 @@ const StyledPicker = styled(EuiFlexItem)` } `; +const H1Text = styled.h1` + white-space: nowrap; +`; + export const PageHeader = React.memo( ({ headingText, extraLinks = false, datePicker = true }: PageHeaderProps) => { const DatePickerComponent = () => @@ -73,7 +78,7 @@ export const PageHeader = React.memo( > -

{headingText}

+ {headingText}
{extraLinkComponents} diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index c8d3ca043edc5..17d79002e6f7d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -32,7 +32,7 @@ export const getMonitorLocations: UMElasticsearchQueryFn< bool: { filter: [ { - match: { + term: { 'monitor.id': monitorId, }, }, @@ -70,6 +70,18 @@ export const getMonitorLocations: UMElasticsearchQueryFn< _source: ['monitor', 'summary', 'observer', '@timestamp'], }, }, + down_history: { + sum: { + field: 'summary.down', + missing: 0, + }, + }, + up_history: { + sum: { + field: 'summary.up', + missing: 0, + }, + }, }, }, }, @@ -99,10 +111,17 @@ export const getMonitorLocations: UMElasticsearchQueryFn< } }; + let totalUps = 0; + let totalDowns = 0; + const monLocs: MonitorLocation[] = []; - locations.forEach((loc: any) => { - const mostRecentLocation = loc.most_recent.hits.hits[0]._source; + locations.forEach(({ most_recent: mostRecent, up_history, down_history }: any) => { + const mostRecentLocation = mostRecent.hits.hits[0]._source; + totalUps += up_history.value; + totalDowns += down_history.value; const location: MonitorLocation = { + up_history: up_history.value ?? 0, + down_history: down_history.value ?? 0, summary: mostRecentLocation?.summary, geo: getGeo(mostRecentLocation?.observer?.geo), timestamp: mostRecentLocation['@timestamp'], @@ -113,5 +132,7 @@ export const getMonitorLocations: UMElasticsearchQueryFn< return { monitorId, locations: monLocs, + up_history: totalUps, + down_history: totalDowns, }; }; diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index dccce4ba1b0b1..ebd120fa0feea 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('detail page', async () => { await uptimeService.navigation.goToMonitor(A11Y_TEST_MONITOR_ID); - await uptimeService.monitor.locationMapIsRendered(); + await uptimeService.monitor.displayOverallAvailability('0.00 %'); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index 27c42a365ec74..8aefca6a70195 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -11,41 +11,58 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const { uptime: uptimePage } = getPageObjects(['uptime']); const uptime = getService('uptime'); + const es = getService('legacyEs'); const monitor = () => uptime.monitor; + const MONITOR_ID = 'location-testing-id'; + + const LessAvailMonitor = 'less-availability-monitor'; + + const addMonitorWithNoLocation = async () => { + /** + * This mogrify function will strip the documents of their location + * data (but preserve their location name), which is necessary for + * this test to work as desired. + * @param d current document + */ + const mogrifyNoLocation = (d: any) => { + if (d.observer?.geo?.location) { + d.observer.geo.location = undefined; + } + return d; + }; + await makeChecksWithStatus(es, MONITOR_ID, 5, 2, 10000, {}, 'up', mogrifyNoLocation); + }; + + const addLessAvailMonitor = async () => { + await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'up'); + await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down'); + }; describe('Observer location', () => { const start = moment().subtract('15', 'm').toISOString(); const end = moment().toISOString(); - const MONITOR_ID = 'location-testing-id'; + before(async () => { + await addMonitorWithNoLocation(); + await addLessAvailMonitor(); + await uptime.navigation.goToUptime(); + await uptimePage.goToRoot(true); + }); beforeEach(async () => { - /** - * This mogrify function will strip the documents of their location - * data (but preserve their location name), which is necessary for - * this test to work as desired. - * @param d current document - */ - const mogrifyNoLocation = (d: any) => { - if (d.observer?.geo?.location) { - d.observer.geo.location = undefined; - } - return d; - }; - await makeChecksWithStatus( - getService('legacyEs'), - MONITOR_ID, - 5, - 2, - 10000, - {}, - 'up', - mogrifyNoLocation - ); - await uptime.navigation.goToUptime(); + await addMonitorWithNoLocation(); + await addLessAvailMonitor(); + if (!(await uptime.navigation.isOnDetailsPage())) + await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); + }); + + it('displays the overall availability', async () => { + await monitor().displayOverallAvailability('100.00 %'); + }); - await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); + it('can change the view to map', async () => { + await monitor().toggleToMapView(); }); it('renders the location panel and canvas', async () => { @@ -55,5 +72,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the location missing popover when monitor has location name, but no geo data', async () => { await monitor().locationMissingExists(); }); + + it('displays less monitor availability', async () => { + await uptime.navigation.goToHomeViaBreadCrumb(); + await uptimePage.loadDataAndGoToMonitorPage(start, end, LessAvailMonitor); + await monitor().displayOverallAvailability('50.00 %'); + }); }); }; diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index b5be1e29a0e8c..5f544b5e46010 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -58,13 +58,13 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) { '[data-test-subj="xpack.uptime.filterBar.filterStatusUp"]' ); if (await upFilter.elementHasClass('euiFilterButton-hasActiveFilters')) { - this.setStatusFilterUp(); + await this.setStatusFilterUp(); } const downFilter = await find.byCssSelector( '[data-test-subj="xpack.uptime.filterBar.filterStatusDown"]' ); if (await downFilter.elementHasClass('euiFilterButton-hasActiveFilters')) { - this.setStatusFilterDown(); + await this.setStatusFilterDown(); } }, async selectFilterItem(filterType: string, option: string) { diff --git a/x-pack/test/functional/services/uptime/monitor.ts b/x-pack/test/functional/services/uptime/monitor.ts index 3137711698ba4..593950fbb7619 100644 --- a/x-pack/test/functional/services/uptime/monitor.ts +++ b/x-pack/test/functional/services/uptime/monitor.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; export function UptimeMonitorProvider({ getService }: FtrProviderContext) { @@ -17,6 +18,13 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { timeout: 3000, }); }, + async displayOverallAvailability(availabilityVal: string) { + return retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('uptimeOverallAvailability'); + const availability = await testSubjects.getVisibleText('uptimeOverallAvailability'); + expect(availability).to.be(availabilityVal); + }); + }, async locationMapIsRendered() { return retry.tryForTime(15000, async () => { await testSubjects.existOrFail('xpack.uptime.locationMap.embeddedPanel', { @@ -45,5 +53,8 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { ); }); }, + async toggleToMapView() { + await testSubjects.click('uptimeMonitorToggleMapBtn'); + }, }; } diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 7c5a4632f8627..f8e0c0cff41f4 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -56,6 +56,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }, goToMonitor: async (monitorId: string) => { + // only go to monitor page if not already there if (!(await testSubjects.exists('uptimeMonitorPage', { timeout: 0 }))) { await testSubjects.click(`monitor-page-link-${monitorId}`); await testSubjects.existOrFail('uptimeMonitorPage', { @@ -80,5 +81,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd); await this.goToMonitor(monitorId); }, + + async isOnDetailsPage() { + return await testSubjects.exists('uptimeMonitorPage', { timeout: 0 }); + }, + + async goToHomeViaBreadCrumb() { + await testSubjects.click('breadcrumb first'); + }, }; } From 9c881447270926c5fb63153c392871cd22eb908a Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 16 Jun 2020 09:30:02 -0700 Subject: [PATCH 20/68] [DOCS] Adds classification to data frame analytics overview (#69068) --- docs/user/ml/index.asciidoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index f82bb0a406511..1bc74ce87de08 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -87,12 +87,14 @@ and {ml-docs}/xpack-ml.html[{ml-cap} {anomaly-detect}]. [[xpack-ml-dfanalytics]] == {dfanalytics-cap} +experimental[] + The Elastic {ml} {dfanalytics} feature enables you to analyze your data using -{oldetection} and {regression} algorithms and generate new indices that contain -the results alongside your source data. +{classification}, {oldetection}, and {regression} algorithms and generate new +indices that contain the results alongside your source data. If you have a license that includes the {ml-features}, you can create -{oldetection} {dfanalytics-jobs} and view their results on the *Analytics* page +{dfanalytics-jobs} and view their results on the *Analytics* page in {kib}. For example: [role="screenshot"] From fb97d91789faf2c7fe9771a832876e4c38020492 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 16 Jun 2020 18:44:34 +0200 Subject: [PATCH 21/68] Bump react-redux (#69182) --- package.json | 4 ++-- x-pack/package.json | 4 ++-- yarn.lock | 17 ++++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b886e6ae0f5d3..85eba40c6251a 100644 --- a/package.json +++ b/package.json @@ -243,7 +243,7 @@ "react-input-range": "^1.3.0", "react-markdown": "^4.3.1", "react-monaco-editor": "~0.27.0", - "react-redux": "^7.1.3", + "react-redux": "^7.2.0", "react-resize-detector": "^4.2.0", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", @@ -375,7 +375,7 @@ "@types/react": "^16.9.36", "@types/react-dom": "^16.9.8", "@types/react-grid-layout": "^0.16.7", - "@types/react-redux": "^7.1.7", + "@types/react-redux": "^7.1.9", "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", diff --git a/x-pack/package.json b/x-pack/package.json index c582cc8be9bbd..e24d75cc0d968 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -98,7 +98,7 @@ "@types/react": "^16.9.36", "@types/react-beautiful-dnd": "^12.1.1", "@types/react-dom": "^16.9.8", - "@types/react-redux": "^7.1.7", + "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.3", "@types/react-sticky": "^6.0.3", "@types/react-test-renderer": "^16.9.1", @@ -322,7 +322,7 @@ "react-markdown": "^4.3.1", "react-moment-proptypes": "^1.7.0", "react-portal": "^3.2.0", - "react-redux": "^7.1.3", + "react-redux": "^7.2.0", "react-reverse-portal": "^1.0.4", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", diff --git a/yarn.lock b/yarn.lock index 0188957073965..e43cb2997e531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5550,10 +5550,10 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react-redux@^7.1.7": - version "7.1.7" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a" - integrity sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg== +"@types/react-redux@^7.1.9": + version "7.1.9" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3" + integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -25452,14 +25452,13 @@ react-redux@^5.1.2: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react-redux@^7.1.0, react-redux@^7.1.1, react-redux@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" - integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== +react-redux@^7.1.0, react-redux@^7.1.1, react-redux@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== dependencies: "@babel/runtime" "^7.5.5" hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" loose-envify "^1.4.0" prop-types "^15.7.2" react-is "^16.9.0" From 62f25506d384fc0c430486fa97c9a5c98e597264 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 16 Jun 2020 09:54:41 -0700 Subject: [PATCH 22/68] [kbn/es] only make one attempt in tests to avoid timeout (#69197) Co-authored-by: spalger Co-authored-by: Elastic Machine --- packages/kbn-es/src/utils/native_realm.js | 30 ++++++++++--------- .../kbn-es/src/utils/native_realm.test.js | 10 +++---- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index 573944a8cc6d0..5075cfb965c08 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -37,13 +37,8 @@ exports.NativeRealm = class NativeRealm { this._log = log; } - async setPassword(username, password = this._elasticPassword, { attempt = 1 } = {}) { - await this._autoRetry(async () => { - this._log.info( - (attempt > 1 ? `attempt ${attempt}: ` : '') + - `setting ${chalk.bold(username)} password to ${chalk.bold(password)}` - ); - + async setPassword(username, password = this._elasticPassword, retryOpts = {}) { + await this._autoRetry(retryOpts, async () => { try { await this._client.security.changePassword({ username, @@ -83,8 +78,8 @@ exports.NativeRealm = class NativeRealm { ); } - async getReservedUsers() { - return await this._autoRetry(async () => { + async getReservedUsers(retryOpts = {}) { + return await this._autoRetry(retryOpts, async () => { const resp = await this._client.security.getUser(); const usernames = Object.keys(resp.body).filter( (user) => resp.body[user].metadata._reserved === true @@ -98,9 +93,9 @@ exports.NativeRealm = class NativeRealm { }); } - async isSecurityEnabled() { + async isSecurityEnabled(retryOpts = {}) { try { - return await this._autoRetry(async () => { + return await this._autoRetry(retryOpts, async () => { const { body: { features }, } = await this._client.xpack.info({ categories: 'features' }); @@ -115,18 +110,25 @@ exports.NativeRealm = class NativeRealm { } } - async _autoRetry(fn, attempt = 1) { + async _autoRetry(opts, fn) { + const { attempt = 1, maxAttempts = 3 } = opts; + try { return await fn(attempt); } catch (error) { - if (attempt >= 3) { + if (attempt >= maxAttempts) { throw error; } const sec = 1.5 * attempt; this._log.warning(`assuming ES isn't initialized completely, trying again in ${sec} seconds`); await new Promise((resolve) => setTimeout(resolve, sec * 1000)); - return await this._autoRetry(fn, attempt + 1); + + const nextOpts = { + ...opts, + attempt: attempt + 1, + }; + return await this._autoRetry(nextOpts, fn); } } }; diff --git a/packages/kbn-es/src/utils/native_realm.test.js b/packages/kbn-es/src/utils/native_realm.test.js index 54732f7136fcc..cd124c97dfdd4 100644 --- a/packages/kbn-es/src/utils/native_realm.test.js +++ b/packages/kbn-es/src/utils/native_realm.test.js @@ -85,7 +85,7 @@ describe('isSecurityEnabled', () => { throw error; }); - expect(await nativeRealm.isSecurityEnabled()).toBe(false); + expect(await nativeRealm.isSecurityEnabled({ maxAttempts: 1 })).toBe(false); }); test('rejects if unexpected error is thrown', async () => { @@ -97,9 +97,9 @@ describe('isSecurityEnabled', () => { throw error; }); - await expect(nativeRealm.isSecurityEnabled()).rejects.toThrowErrorMatchingInlineSnapshot( - `"ResponseError"` - ); + await expect( + nativeRealm.isSecurityEnabled({ maxAttempts: 1 }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"ResponseError"`); }); }); @@ -226,7 +226,7 @@ describe('setPassword', () => { }); await expect( - nativeRealm.setPassword('kibana_system', 'foo') + nativeRealm.setPassword('kibana_system', 'foo', { maxAttempts: 1 }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"SomeError"`); }); }); From 3de93501c6df2007312e22dc872135e039f1ff0d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 16 Jun 2020 12:55:18 -0400 Subject: [PATCH 23/68] Resolve security cloud test failures (#68935) Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../functional/page_objects/security_page.ts | 173 ++++++++---------- 1 file changed, 72 insertions(+), 101 deletions(-) diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 57f62ab33e39a..395e41a91f53a 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -16,6 +16,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const userMenu = getService('userMenu'); + const comboBox = getService('comboBox'); const PageObjects = getPageObjects(['common', 'header', 'settings', 'home', 'error']); interface LoginOptions { @@ -273,11 +274,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider async addIndexToRole(index: string) { log.debug(`Adding index ${index} to role`); - const indexInput = await retry.try(() => - find.byCssSelector('[data-test-subj="indicesInput0"] input') - ); - await indexInput.type(index); - await indexInput.type('\n'); + await comboBox.setCustom('indicesInput0', index); } async addPrivilegeToRole(privilege: string) { @@ -400,104 +397,78 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } } - addRole(roleName: string, roleObj: Role) { + async addRole(roleName: string, roleObj: Role) { const self = this; - return ( - this.clickNewRole() - .then(function () { - // We have to use non-test-subject selectors because this markup is generated by ui-select. - log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); - return testSubjects.append('roleFormNameInput', roleName); - }) - .then(function () { - return find.setValue( - '[data-test-subj="indicesInput0"] input', - roleObj.elasticsearch.indices[0].names + '\n' - ); - }) - .then(function () { - return testSubjects.click('restrictDocumentsQuery0'); - }) - .then(function () { - if (roleObj.elasticsearch.indices[0].query) { - return testSubjects.setValue('queryInput0', roleObj.elasticsearch.indices[0].query); - } - }) - - // KibanaPrivilege - .then(async () => { - const globalPrivileges = (roleObj.kibana as any).global; - if (globalPrivileges) { - for (const privilegeName of globalPrivileges) { - const button = await testSubjects.find('addSpacePrivilegeButton'); - await button.click(); - - const spaceSelector = await testSubjects.find('spaceSelectorComboBox'); - await spaceSelector.click(); - - const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`); - await globalSpaceOption.click(); - - const basePrivilegeSelector = await testSubjects.find('basePrivilegeComboBox'); - await basePrivilegeSelector.click(); - - const privilegeOption = await find.byCssSelector(`#basePrivilege_${privilegeName}`); - await privilegeOption.click(); - - const createPrivilegeButton = await testSubjects.find('createSpacePrivilegeButton'); - await createPrivilegeButton.click(); - } - } - }) - - .then(function () { - function addPrivilege(privileges: string[]) { - return privileges.reduce(function (promise: Promise, privilegeName: string) { - // We have to use non-test-subject selectors because this markup is generated by ui-select. - return promise - .then(() => self.addPrivilegeToRole(privilegeName)) - .then(() => PageObjects.common.sleep(250)); - }, Promise.resolve()); - } - return addPrivilege(roleObj.elasticsearch.indices[0].privileges); - }) - // clicking the Granted fields and removing the asterix - .then(async function () { - function addGrantedField(field: string[]) { - return field.reduce(function (promise: Promise, fieldName: string) { - return promise - .then(function () { - return find.setValue('[data-test-subj="fieldInput0"] input', fieldName + '\n'); - }) - .then(function () { - return PageObjects.common.sleep(1000); - }); - }, Promise.resolve()); - } - - if (roleObj.elasticsearch.indices[0].field_security) { - // Toggle FLS switch - await testSubjects.click('restrictFieldsQuery0'); - - // have to remove the '*' - return find - .clickByCssSelector( - 'div[data-test-subj="fieldInput0"] [title="Remove * from selection in this group"] svg.euiIcon' - ) - .then(function () { - return addGrantedField(roleObj.elasticsearch.indices[0].field_security!.grant!); - }); - } - }) // clicking save button - .then(async () => { - log.debug('click save button'); - await testSubjects.click('roleFormSaveButton'); - }) - .then(function () { - return PageObjects.common.sleep(5000); - }) - ); + await this.clickNewRole(); + + // We have to use non-test-subject selectors because this markup is generated by ui-select. + log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); + await testSubjects.append('roleFormNameInput', roleName); + + for (const indexName of roleObj.elasticsearch.indices[0].names) { + await comboBox.setCustom('indicesInput0', indexName); + } + + if (roleObj.elasticsearch.indices[0].query) { + await testSubjects.click('restrictDocumentsQuery0'); + await testSubjects.setValue('queryInput0', roleObj.elasticsearch.indices[0].query); + } + + const globalPrivileges = (roleObj.kibana as any).global; + if (globalPrivileges) { + for (const privilegeName of globalPrivileges) { + await testSubjects.click('addSpacePrivilegeButton'); + + await testSubjects.click('spaceSelectorComboBox'); + + const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`); + await globalSpaceOption.click(); + + await testSubjects.click('basePrivilegeComboBox'); + + const privilegeOption = await find.byCssSelector(`#basePrivilege_${privilegeName}`); + await privilegeOption.click(); + + await testSubjects.click('createSpacePrivilegeButton'); + } + } + + function addPrivilege(privileges: string[]) { + return privileges.reduce(function (promise: Promise, privilegeName: string) { + return promise + .then(() => self.addPrivilegeToRole(privilegeName)) + .then(() => PageObjects.common.sleep(250)); + }, Promise.resolve()); + } + + await addPrivilege(roleObj.elasticsearch.indices[0].privileges); + + async function addGrantedField(fields: string[]) { + for (const entry of fields) { + await comboBox.setCustom('fieldInput0', entry); + } + } + + // clicking the Granted fields and removing the asterix + if (roleObj.elasticsearch.indices[0].field_security) { + // Toggle FLS switch + await testSubjects.click('restrictFieldsQuery0'); + + // have to remove the '*' + await find.clickByCssSelector( + 'div[data-test-subj="fieldInput0"] [title="Remove * from selection in this group"] svg.euiIcon' + ); + + await addGrantedField(roleObj.elasticsearch.indices[0].field_security!.grant!); + } + + log.debug('click save button'); + await testSubjects.click('roleFormSaveButton'); + + // Signifies that the role management page redirected back to the role grid page, + // and successfully refreshed the grid + await testSubjects.existOrFail('roleRow'); } async selectRole(role: string) { From d5785a0d6d91f6dd110ea2d2cb69e6c3bfded427 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 16 Jun 2020 12:02:40 -0600 Subject: [PATCH 24/68] SIEM] Moves validation up to the common section ## Summary Moves validation up to the common section so it can be used by others in common for API boundary validation. --- .../update_exception_list_item_schema.test.ts | 2 +- .../exception_list_item_schema.test.ts | 2 +- .../plugins/lists/common/siem_common_deps.ts | 2 ++ .../create_exception_list_item_route.ts | 8 ++--- .../routes/create_exception_list_route.ts | 8 ++--- .../server/routes/create_list_index_route.ts | 3 +- .../server/routes/create_list_item_route.ts | 8 ++--- .../lists/server/routes/create_list_route.ts | 8 ++--- .../delete_exception_list_item_route.ts | 8 ++--- .../routes/delete_exception_list_route.ts | 8 ++--- .../server/routes/delete_list_index_route.ts | 3 +- .../server/routes/delete_list_item_route.ts | 8 ++--- .../lists/server/routes/delete_list_route.ts | 8 ++--- .../routes/find_exception_list_item_route.ts | 8 ++--- .../routes/find_exception_list_route.ts | 8 ++--- .../server/routes/find_list_item_route.ts | 8 ++--- .../lists/server/routes/find_list_route.ts | 8 ++--- .../server/routes/import_list_item_route.ts | 8 ++--- .../server/routes/patch_list_item_route.ts | 8 ++--- .../lists/server/routes/patch_list_route.ts | 8 ++--- .../routes/read_exception_list_item_route.ts | 8 ++--- .../routes/read_exception_list_route.ts | 8 ++--- .../server/routes/read_list_index_route.ts | 3 +- .../server/routes/read_list_item_route.ts | 8 ++--- .../lists/server/routes/read_list_route.ts | 8 ++--- .../update_exception_list_item_route.ts | 8 ++--- .../routes/update_exception_list_route.ts | 8 ++--- .../server/routes/update_list_item_route.ts | 8 ++--- .../lists/server/routes/update_list_route.ts | 8 ++--- .../plugins/lists/server/siem_server_deps.ts | 1 - .../security_solution/common/validate.test.ts | 34 +++++++++++++++++++ .../security_solution/common/validate.ts | 25 ++++++++++++++ .../plugins/security_solution/server/index.ts | 1 - .../rules/add_prepackaged_rules_route.ts | 2 +- .../routes/rules/create_rules_bulk_route.ts | 3 +- .../routes/rules/delete_rules_bulk_route.ts | 3 +- .../get_prepackaged_rules_status_route.ts | 2 +- .../routes/rules/import_rules_route.ts | 2 +- .../routes/rules/patch_rules_bulk_route.ts | 3 +- .../routes/rules/update_rules_bulk_route.ts | 3 +- .../routes/rules/validate.test.ts | 23 ------------- .../detection_engine/routes/rules/validate.ts | 15 +------- .../timeline/routes/import_timelines_route.ts | 2 +- 43 files changed, 128 insertions(+), 190 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/validate.test.ts create mode 100644 x-pack/plugins/security_solution/common/validate.ts diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index 38541e205598b..69702a5e8a4f9 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -147,7 +147,7 @@ describe('update_exception_list_item_schema', () => { // TODO: Is it expected behavior for it not to auto-generate a uui or throw // error if item_id is not passed in? - xtest('it should accept an undefined for "item_id" and auto generate a uuid', () => { + test.skip('it should accept an undefined for "item_id" and auto generate a uuid', () => { const inputPayload = getUpdateExceptionListItemSchemaMock(); delete inputPayload.item_id; const decoded = updateExceptionListItemSchema.decode(inputPayload); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index b9d142fbccbee..ff900104251b7 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -101,7 +101,7 @@ describe('exception_list_item_schema', () => { // TODO: Should this throw an error? "namespace_type" gets auto-populated // with default "single", is that desired behavior? - xtest('it should NOT accept an undefined for "namespace_type"', () => { + test.skip('it should NOT accept an undefined for "namespace_type"', () => { const payload = getExceptionListItemSchemaMock(); delete payload.namespace_type; const decoded = exceptionListItemSchema.decode(payload); diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts index 3759305987f79..b1bb7d8aace36 100644 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -9,3 +9,5 @@ export { DefaultUuid } from '../../security_solution/common/detection_engine/sch export { DefaultStringArray } from '../../security_solution/common/detection_engine/schemas/types/default_string_array'; export { exactCheck } from '../../security_solution/common/exact_check'; export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils'; +export { validate } from '../../security_solution/common/validate'; +export { formatErrors } from '../../security_solution/common/format_errors'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index 2cafd435e0853..375d25c6fa5f8 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index 9be6b72dcd255..bd29a65c9450a 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 1c893fb757c5d..5ec2b36da61b0 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -6,7 +6,8 @@ import { IRouter } from 'kibana/server'; -import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 68622e98cbc52..2e1b7fa07221f 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -7,13 +7,9 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; +import { validate } from '../../common/siem_common_deps'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 0f3c404c53cfd..9872bbfa09e23 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 2c91fe3c28681..f363252dada50 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index b4c67c0ab1418..b1bf705dcc5f6 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index 424c3f45aac40..cb2e16b3602a7 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -7,7 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; -import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 82dfe8a4f29d0..510be764cefba 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index e89355b7689c5..600e4b00c29ca 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { deleteListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 1820ffdeadb88..a6c2a18bb8c8a 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 3181deda8b91d..97e1de834cd37 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 37b5fe44b919c..1ccb948d0ad21 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { findListItemSchema, foundListItemSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 04b33e3d67075..2fa43c6368b5c 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index c951c9b337131..67f345c2c6c1d 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,12 +9,8 @@ import { Readable } from 'stream'; import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { importListItemQuerySchema, importListItemSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index e18fd0618b133..f706559dffdbd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 9d3fa4db8ccd0..3a0d8714a14cd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index 083d4d7a0d479..c4e969b27fcf4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index c295f045b38c2..6cb91c10aea55 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 21f539d97fc74..4664bed3e7a8b 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -7,7 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; -import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index 10c7f781f554c..24011d3b50d27 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -7,13 +7,9 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; +import { validate } from '../../common/siem_common_deps'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index c30eadfca0b65..34924b70fd4df 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 73392c326056e..0ec33b7651982 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index fe45d403c040f..cff78614d05ba 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index 494d57b93b8e4..3e231e319104b 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 6ace61e46a780..a6d9f8329c7c8 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -7,12 +7,8 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; -import { - buildRouteValidation, - buildSiemResponse, - transformError, - validate, -} from '../siem_server_deps'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts index df4b07fc46322..87a623c7a1892 100644 --- a/x-pack/plugins/lists/server/siem_server_deps.ts +++ b/x-pack/plugins/lists/server/siem_server_deps.ts @@ -17,5 +17,4 @@ export { createBootstrapIndex, getIndexExists, buildRouteValidation, - validate, } from '../../security_solution/server'; diff --git a/x-pack/plugins/security_solution/common/validate.test.ts b/x-pack/plugins/security_solution/common/validate.test.ts new file mode 100644 index 0000000000000..032f6d9590168 --- /dev/null +++ b/x-pack/plugins/security_solution/common/validate.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { validate } from './validate'; + +describe('validate', () => { + test('it should do a validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 1 }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(payload); + expect(errors).toEqual(null); + }); + + test('it should do an in-validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 'some other value' }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(null); + expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/validate.ts b/x-pack/plugins/security_solution/common/validate.ts new file mode 100644 index 0000000000000..db9e286e2ebc2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/validate.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; +import { exactCheck } from './exact_check'; +import { formatErrors } from './format_errors'; + +export const validate = ( + obj: object, + schema: T +): [t.TypeOf | null, string | null] => { + const decoded = schema.decode(obj); + const checked = exactCheck(obj, decoded); + const left = (errors: t.Errors): [T | null, string | null] => [ + null, + formatErrors(errors).join(','), + ]; + const right = (output: T): [T | null, string | null] => [output, null]; + return pipe(checked, fold(left, right)); +}; diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 586b9dec2f4ab..8a77137c20c11 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -27,5 +27,4 @@ export { getPolicyExists } from './lib/detection_engine/index/get_policy_exists' export { createBootstrapIndex } from './lib/detection_engine/index/create_bootstrap_index'; export { getIndexExists } from './lib/detection_engine/index/get_index_exists'; export { buildRouteValidation } from './utils/build_validation/route_validation'; -export { validate } from './lib/detection_engine/routes/rules/validate'; export { transformError, buildSiemResponse } from './lib/detection_engine/routes/utils'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 39eea16c6290a..9878521c49322 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { PrePackagedRulesSchema, prePackagedRulesSchema, @@ -18,7 +19,6 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { validate } from './validate'; export const addPrepackedRulesRoute = (router: IRouter) => { router.put( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 7af58adca7529..92a7ea17e7eaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { @@ -19,7 +20,7 @@ import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; import { getDuplicates } from './utils'; -import { transformValidateBulkError, validate } from './validate'; +import { transformValidateBulkError } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 12f908ce7e8b5..99bf16aadc815 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { @@ -14,7 +15,7 @@ import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/ import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdBulkError } from './utils'; -import { transformValidateBulkError, validate } from './validate'; +import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index c3f4695a20461..bc199ee132e96 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, @@ -16,7 +17,6 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { validate } from './validate'; export const getPrepackagedRulesStatusRoute = (router: IRouter) => { router.get( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index c8b61414608a9..a277f97ccf9f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,6 +7,7 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; +import { validate } from '../../../../../common/validate'; import { importRulesQuerySchema, ImportRulesQuerySchemaDecoded, @@ -39,7 +40,6 @@ import { } from '../utils'; import { patchRules } from '../../rules/patch_rules'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; -import { validate } from './validate'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { HapiReadableStream } from '../../rules/types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 16f491547a9e6..b2a9fdd103a68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { patchRulesBulkSchema, @@ -18,7 +19,7 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { transformBulkError, buildSiemResponse } from '../utils'; import { getIdBulkError } from './utils'; -import { transformValidateBulkError, validate } from './validate'; +import { transformValidateBulkError } from './validate'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 3ca4a28dd93ee..1e6815a357154 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { validate } from '../../../../../common/validate'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -18,7 +19,7 @@ import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { getIdBulkError } from './utils'; -import { transformValidateBulkError, validate } from './validate'; +import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 07b891e2bf021..1f5442e23d884 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; - import { - validate, transformValidate, transformValidateFindAlerts, transformValidateBulkError, @@ -121,26 +118,6 @@ describe('validate', () => { unSetFeatureFlagsForTestsOnly(); }); - describe('validate', () => { - test('it should do a validation correctly', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 1 }; - const [validated, errors] = validate(payload, schema); - - expect(validated).toEqual(payload); - expect(errors).toEqual(null); - }); - - test('it should do an in-validation correctly', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 'some other value' }; - const [validated, errors] = validate(payload, schema); - - expect(validated).toEqual(null); - expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); - }); - }); - describe('transformValidate', () => { test('it should do a validation correctly of a partial alert', () => { const ruleAlert = getResult(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 7b0bf5997d12f..983382b28ab38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -9,6 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { validate } from '../../../../../common/validate'; import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; import { RulesSchema, @@ -113,17 +114,3 @@ export const transformValidateBulkError = ( }); } }; - -export const validate = ( - obj: object, - schema: T -): [t.TypeOf | null, string | null] => { - const decoded = schema.decode(obj); - const checked = exactCheck(obj, decoded); - const left = (errors: t.Errors): [T | null, string | null] => [ - null, - formatErrors(errors).join(','), - ]; - const right = (output: T): [T | null, string | null] => [output, null]; - return pipe(checked, fold(left, right)); -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index 0f9e97cfc2106..5080142f22b15 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -7,6 +7,7 @@ import { extname } from 'path'; import { chunk, omit } from 'lodash/fp'; +import { validate } from '../../../../common/validate'; import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema'; import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; import { IRouter } from '../../../../../../../src/core/server'; @@ -17,7 +18,6 @@ import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, createBulkErrorObject, From 9f7620b84dd0707666b6c2d20e3c98b1d0d8eb03 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 16 Jun 2020 20:15:01 +0200 Subject: [PATCH 25/68] [QA] Code coverage: fix flaky tests (#69272) * skip test * [page_objects/common_page] update closing toast --- test/functional/apps/dashboard/dashboard_filter_bar.js | 2 ++ test/functional/page_objects/common_page.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index c931e6763f483..f9b0d0a370c06 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -170,6 +170,8 @@ export default function ({ getService, getPageObjects }) { }); describe('saved search filtering', function () { + // https://github.com/elastic/kibana/issues/47286#issuecomment-644687577 + this.tags('skipCoverage'); before(async () => { await filterBar.ensureFieldEditorModalIsClosed(); await PageObjects.dashboard.gotoDashboardLandingPage(); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index fe5694efc35da..d08e88ecf47ea 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -409,7 +409,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async closeToastIfExists() { const toastShown = await find.existsByCssSelector('.euiToast'); if (toastShown) { - await this.closeToast(); + await find.clickByCssSelector('.euiToast__closeButton'); } } From efbb4ccc31f314f51b2f10663a5635cbed78e148 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 16 Jun 2020 14:29:11 -0400 Subject: [PATCH 26/68] use navigateToUrl instead of window location (#69167) --- .../back_to_list_panel/back_to_list_panel.tsx | 57 +++++++++++-------- .../analytics_list/action_clone.tsx | 18 ++++-- .../source_selection/source_selection.tsx | 18 ++++-- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index e437d27372a3e..b6b335afa53f5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,29 +7,38 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../../contexts/kibana'; -function redirectToAnalyticsManagementPage() { - window.location.href = '#/data_frame_analytics?'; -} +export const BackToListPanel: FC = () => { + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); -export const BackToListPanel: FC = () => ( - - - } - title={i18n.translate('xpack.ml.dataframe.analytics.create.analyticsListCardTitle', { - defaultMessage: 'Data Frame Analytics', - })} - description={i18n.translate( - 'xpack.ml.dataframe.analytics.create.analyticsListCardDescription', - { - defaultMessage: 'Return to the analytics management page.', - } - )} - onClick={redirectToAnalyticsManagementPage} - data-test-subj="analyticsWizardCardManagement" - /> - -); + const redirectToAnalyticsManagementPage = async () => { + await navigateToUrl('#/data_frame_analytics?'); + }; + + return ( + + + } + title={i18n.translate('xpack.ml.dataframe.analytics.create.analyticsListCardTitle', { + defaultMessage: 'Data Frame Analytics', + })} + description={i18n.translate( + 'xpack.ml.dataframe.analytics.create.analyticsListCardDescription', + { + defaultMessage: 'Return to the analytics management page.', + } + )} + onClick={redirectToAnalyticsManagementPage} + data-test-subj="analyticsWizardCardManagement" + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index e8b1cd1a5696a..df7dce7217fd4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -360,7 +360,14 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) defaultMessage: 'Clone job', }); - const { notifications, savedObjects } = useMlKibana().services; + const { + services: { + application: { navigateToUrl }, + notifications: { toasts }, + savedObjects, + }, + } = useMlKibana(); + const savedObjectsClient = savedObjects.client; const onClick = async () => { @@ -385,7 +392,6 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) sourceIndexId = ip.id; } } catch (e) { - const { toasts } = notifications; const error = extractErrorMessage(e); toasts.addDanger( @@ -401,9 +407,11 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) } if (sourceIndexId) { - window.location.href = `ml#/data_frame_analytics/new_job?index=${encodeURIComponent( - sourceIndexId - )}&jobId=${item.config.id}`; + await navigateToUrl( + `ml#/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ + item.config.id + }` + ); } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index d20afe93d2b9d..b03a58a02309d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -26,12 +26,20 @@ interface Props { } export const SourceSelection: FC = ({ onClose }) => { - const { uiSettings, savedObjects } = useMlKibana().services; + const { + services: { + application: { navigateToUrl }, + savedObjects, + uiSettings, + }, + } = useMlKibana(); - const onSearchSelected = (id: string, type: string) => { - window.location.href = `ml#/data_frame_analytics/new_job?${ - type === 'index-pattern' ? 'index' : 'savedSearchId' - }=${encodeURIComponent(id)}`; + const onSearchSelected = async (id: string, type: string) => { + await navigateToUrl( + `ml#/data_frame_analytics/new_job?${ + type === 'index-pattern' ? 'index' : 'savedSearchId' + }=${encodeURIComponent(id)}` + ); }; return ( From db1df7bed03f624dca78630830b9bb0395666b20 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 16 Jun 2020 12:57:34 -0700 Subject: [PATCH 27/68] [kbn/optimizer] fix windows compatibility (#69304) Co-authored-by: spalger --- .../src/optimizer/observe_worker.ts | 66 +++++++++++++++---- .../src/worker/entry_point_creator.ts | 10 ++- .../kbn-optimizer/src/worker/run_worker.ts | 38 +++++++++-- .../src/worker/webpack.config.ts | 14 ++-- 4 files changed, 102 insertions(+), 26 deletions(-) diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index 4527052fa821a..fef3efc13a516 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -22,7 +22,7 @@ import { inspect } from 'util'; import execa from 'execa'; import * as Rx from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { map, takeUntil, first, ignoreElements } from 'rxjs/operators'; import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle, BundleRefs } from '../common'; @@ -68,19 +68,11 @@ if (inspectFlagIndex !== -1) { function usingWorkerProc( config: OptimizerConfig, - workerConfig: WorkerConfig, - bundles: Bundle[], fn: (proc: execa.ExecaChildProcess) => Rx.Observable ) { return Rx.using( (): ProcResource => { - const args = [ - JSON.stringify(workerConfig), - JSON.stringify(bundles.map((b) => b.toSpec())), - BundleRefs.fromBundles(config.bundles).toSpecJson(), - ]; - - const proc = execa.node(require.resolve('../worker/run_worker'), args, { + const proc = execa.node(require.resolve('../worker/run_worker'), [], { nodeOptions: [ ...(inspectFlag && config.inspectWorkers ? [`${inspectFlag}=${inspectPortCounter++}`] @@ -129,6 +121,51 @@ function observeStdio$(stream: Readable, name: WorkerStdio['stream']) { ); } +/** + * We used to pass configuration to the worker as JSON encoded arguments, but they + * grew too large for argv, especially on Windows, so we had to move to an async init + * where we send the args over IPC. To keep the logic simple we basically mock the + * argv behavior and don't use complicated messages or anything so that state can + * be initialized in the worker before most of the code is run. + */ +function initWorker( + proc: execa.ExecaChildProcess, + config: OptimizerConfig, + workerConfig: WorkerConfig, + bundles: Bundle[] +) { + const msg$ = Rx.fromEvent<[unknown]>(proc, 'message').pipe( + // validate the initialization messages from the process + map(([msg]) => { + if (typeof msg === 'string') { + switch (msg) { + case 'init': + return 'init' as const; + case 'ready': + return 'ready' as const; + } + } + + throw new Error(`unexpected message from worker while initializing: [${inspect(msg)}]`); + }) + ); + + return Rx.concat( + msg$.pipe(first((msg) => msg === 'init')), + Rx.defer(() => { + proc.send({ + args: [ + JSON.stringify(workerConfig), + JSON.stringify(bundles.map((b) => b.toSpec())), + BundleRefs.fromBundles(config.bundles).toSpecJson(), + ], + }); + return []; + }), + msg$.pipe(first((msg) => msg === 'ready')) + ).pipe(ignoreElements()); +} + /** * Start a worker process with the specified `workerConfig` and * `bundles` and return an observable of the events related to @@ -140,10 +177,11 @@ export function observeWorker( workerConfig: WorkerConfig, bundles: Bundle[] ): Rx.Observable { - return usingWorkerProc(config, workerConfig, bundles, (proc) => { - let lastMsg: WorkerMsg; + return usingWorkerProc(config, (proc) => { + const init$ = initWorker(proc, config, workerConfig, bundles); - return Rx.merge( + let lastMsg: WorkerMsg; + const worker$: Rx.Observable = Rx.merge( Rx.of({ type: 'worker started', bundles, @@ -201,5 +239,7 @@ export function observeWorker( ) ) ); + + return Rx.concat(init$, worker$); }); } diff --git a/packages/kbn-optimizer/src/worker/entry_point_creator.ts b/packages/kbn-optimizer/src/worker/entry_point_creator.ts index a613e3e8925a4..f8f41b2e13422 100644 --- a/packages/kbn-optimizer/src/worker/entry_point_creator.ts +++ b/packages/kbn-optimizer/src/worker/entry_point_creator.ts @@ -17,9 +17,13 @@ * under the License. */ -module.exports = function ({ entries }: { entries: Array<{ importId: string; relPath: string }> }) { - const lines = entries.map(({ importId, relPath }) => [ - `__kbnBundles__.define('${importId}', __webpack_require__, require.resolve('./${relPath}'))`, +module.exports = function ({ + entries, +}: { + entries: Array<{ importId: string; requirePath: string }>; +}) { + const lines = entries.map(({ importId, requirePath }) => [ + `__kbnBundles__.define('${importId}', __webpack_require__, require.resolve('${requirePath}'))`, ]); return { diff --git a/packages/kbn-optimizer/src/worker/run_worker.ts b/packages/kbn-optimizer/src/worker/run_worker.ts index 178637d39ab00..781cf83624a1e 100644 --- a/packages/kbn-optimizer/src/worker/run_worker.ts +++ b/packages/kbn-optimizer/src/worker/run_worker.ts @@ -17,7 +17,10 @@ * under the License. */ +import { inspect } from 'util'; + import * as Rx from 'rxjs'; +import { take, mergeMap } from 'rxjs/operators'; import { parseBundles, @@ -80,15 +83,38 @@ setInterval(() => { } }, 1000).unref(); +function assertInitMsg(msg: unknown): asserts msg is { args: string[] } { + if (typeof msg !== 'object' || !msg) { + throw new Error(`expected init message to be an object: ${inspect(msg)}`); + } + + const { args } = msg as Record; + if (!args || !Array.isArray(args) || !args.every((a) => typeof a === 'string')) { + throw new Error( + `expected init message to have an 'args' property that's an array of strings: ${inspect(msg)}` + ); + } +} + Rx.defer(() => { - const workerConfig = parseWorkerConfig(process.argv[2]); - const bundles = parseBundles(process.argv[3]); - const bundleRefs = BundleRefs.parseSpec(process.argv[4]); + process.send!('init'); + + return Rx.fromEvent<[unknown]>(process as any, 'message').pipe( + take(1), + mergeMap(([msg]) => { + assertInitMsg(msg); + process.send!('ready'); + + const workerConfig = parseWorkerConfig(msg.args[0]); + const bundles = parseBundles(msg.args[1]); + const bundleRefs = BundleRefs.parseSpec(msg.args[2]); - // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers - process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; + // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers + process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; - return runCompilers(workerConfig, bundles, bundleRefs); + return runCompilers(workerConfig, bundles, bundleRefs); + }) + ); }).subscribe( (msg) => { send(msg); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index e361b186c30e0..11f5544cd9274 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -100,10 +100,16 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: entries: bundle.publicDirNames.map((name) => { const absolute = Path.resolve(bundle.contextDir, name); const newContext = Path.dirname(ENTRY_CREATOR); - return { - importId: `${bundle.type}/${bundle.id}/${name}`, - relPath: Path.relative(newContext, absolute), - }; + const importId = `${bundle.type}/${bundle.id}/${name}`; + + // relative path from context of the ENTRY_CREATOR, with linux path separators + let requirePath = Path.relative(newContext, absolute).split('\\').join('/'); + if (!requirePath.startsWith('.')) { + // ensure requirePath is identified by node as relative + requirePath = `./${requirePath}`; + } + + return { importId, requirePath }; }), }, }, From ade4c8dded247e4d29b9eb5b9baa934e214aa676 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 16 Jun 2020 13:20:52 -0700 Subject: [PATCH 28/68] [Search service] Refactor the way server-side search strategies are registered (#68452) * [search] Refactor the way search strategies are registered/retrieved on the server * Fix types and tests and update docs * Fix failing test * Fix build of example plugin * Fix functional test * Make server strategies sync Co-authored-by: Liza K --- ...in-plugins-data-server.irequesttypesmap.md | 2 + ...n-plugins-data-server.iresponsetypesmap.md | 2 + ...bana-plugin-plugins-data-server.isearch.md | 2 +- ...lugin-plugins-data-server.isearchcancel.md | 2 +- ...gins-data-server.isearchcontext.config_.md | 11 -- ...plugins-data-server.isearchcontext.core.md | 11 -- ...ugin-plugins-data-server.isearchcontext.md | 19 --- ...ugin-plugins-data-server.isearchoptions.md | 2 +- ...ugins-data-server.isearchoptions.signal.md | 2 + ...plugin-plugins-data-server.isearchsetup.md | 18 +++ ...ver.isearchsetup.registersearchstrategy.md | 13 ++ ...a-server.isearchstart.getsearchstrategy.md | 13 ++ ...plugin-plugins-data-server.isearchstart.md | 18 +++ ...gins-data-server.isearchstrategy.cancel.md | 11 ++ ...gin-plugins-data-server.isearchstrategy.md | 21 ++++ ...gins-data-server.isearchstrategy.search.md | 11 ++ .../kibana-plugin-plugins-data-server.md | 10 +- ...plugin-plugins-data-server.plugin.setup.md | 8 +- ...plugin-plugins-data-server.plugin.start.md | 6 +- ...-plugin-plugins-data-server.pluginstart.md | 1 + ...-plugins-data-server.pluginstart.search.md | 11 ++ ...ins-data-server.tsearchstrategyprovider.md | 13 -- ...ugin-plugins-data-server.tstrategytypes.md | 19 +++ .../server/async_demo_search_strategy.ts | 50 ++++---- .../server/demo_search_strategy.ts | 8 +- examples/demo_search/server/index.ts | 5 +- examples/demo_search/server/plugin.ts | 15 +-- .../search_explorer/public/search_api.tsx | 13 -- .../mocks.ts} | 13 +- src/plugins/data/server/index.ts | 6 +- .../{search/i_search_context.ts => mocks.ts} | 24 +++- src/plugins/data/server/plugin.ts | 11 +- .../data/server/search/create_api.test.ts | 71 ----------- src/plugins/data/server/search/create_api.ts | 58 --------- .../es_search/es_search_strategy.test.ts | 55 +++------ .../search/es_search/es_search_strategy.ts | 22 ++-- src/plugins/data/server/search/i_search.ts | 55 --------- .../data/server/search/i_search_setup.ts | 40 ------- .../data/server/search/i_search_strategy.ts | 67 ----------- src/plugins/data/server/search/index.ts | 14 +-- src/plugins/data/server/search/mocks.ts | 18 +-- src/plugins/data/server/search/routes.test.ts | 67 ++++------- src/plugins/data/server/search/routes.ts | 19 ++- .../data/server/search/search_service.test.ts | 19 +-- .../data/server/search/search_service.ts | 92 +++++---------- .../data/server/search/strategy_types.ts | 39 ------ src/plugins/data/server/search/types.ts | 111 ++++++++++++++++++ src/plugins/data/server/server.api.md | 75 +++++++----- .../core_plugins/legacy_plugins.ts | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 5 +- .../server/search/es_search_strategy.test.ts | 69 +++-------- .../server/search/es_search_strategy.ts | 24 ++-- 52 files changed, 552 insertions(+), 741 deletions(-) delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.config_.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.core.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.search.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tsearchstrategyprovider.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tstrategytypes.md rename src/plugins/data/server/{search/i_route_handler_search_context.ts => field_formats/mocks.ts} (80%) rename src/plugins/data/server/{search/i_search_context.ts => mocks.ts} (60%) delete mode 100644 src/plugins/data/server/search/create_api.test.ts delete mode 100644 src/plugins/data/server/search/create_api.ts delete mode 100644 src/plugins/data/server/search/i_search.ts delete mode 100644 src/plugins/data/server/search/i_search_setup.ts delete mode 100644 src/plugins/data/server/search/i_search_strategy.ts delete mode 100644 src/plugins/data/server/search/strategy_types.ts create mode 100644 src/plugins/data/server/search/types.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.irequesttypesmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.irequesttypesmap.md index a9bb8f1eb9d6d..3f5e4ba0f7799 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.irequesttypesmap.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.irequesttypesmap.md @@ -4,6 +4,8 @@ ## IRequestTypesMap interface +The map of search strategy IDs to the corresponding request type definitions. + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iresponsetypesmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iresponsetypesmap.md index fe5fa0a5d3a33..629ab4347eda8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iresponsetypesmap.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iresponsetypesmap.md @@ -4,6 +4,8 @@ ## IResponseTypesMap interface +The map of search strategy IDs to the corresponding response type definitions. + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearch.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearch.md index 6e037f5161b53..96991579c1716 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearch.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearch.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type ISearch = (request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; +export declare type ISearch = (context: RequestHandlerContext, request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md index 99c30515e8da6..b5a687d1b19d8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type ISearchCancel = (id: string) => Promise; +export declare type ISearchCancel = (context: RequestHandlerContext, id: string) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.config_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.config_.md deleted file mode 100644 index 364d44dba758a..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.config_.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchContext](./kibana-plugin-plugins-data-server.isearchcontext.md) > [config$](./kibana-plugin-plugins-data-server.isearchcontext.config_.md) - -## ISearchContext.config$ property - -Signature: - -```typescript -config$: Observable; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.core.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.core.md deleted file mode 100644 index 9d571c25d94bd..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchContext](./kibana-plugin-plugins-data-server.isearchcontext.md) > [core](./kibana-plugin-plugins-data-server.isearchcontext.core.md) - -## ISearchContext.core property - -Signature: - -```typescript -core: CoreSetup; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.md deleted file mode 100644 index 1c3c5ec78f894..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcontext.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchContext](./kibana-plugin-plugins-data-server.isearchcontext.md) - -## ISearchContext interface - -Signature: - -```typescript -export interface ISearchContext -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [config$](./kibana-plugin-plugins-data-server.isearchcontext.config_.md) | Observable<SharedGlobalConfig> | | -| [core](./kibana-plugin-plugins-data-server.isearchcontext.core.md) | CoreSetup | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 0319048f4418b..49412fc42d3b5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -14,5 +14,5 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | -| [signal](./kibana-plugin-plugins-data-server.isearchoptions.signal.md) | AbortSignal | | +| [signal](./kibana-plugin-plugins-data-server.isearchoptions.signal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md index 7da5c182b2e0f..948dfd66da7a0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md @@ -4,6 +4,8 @@ ## ISearchOptions.signal property +An `AbortSignal` that allows the caller of `search` to abort a search request. + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md new file mode 100644 index 0000000000000..93e253b2e98a3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) + +## ISearchSetup interface + +Signature: + +```typescript +export interface ISearchSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | TRegisterSearchStrategy | Extension point exposed for other plugins to register their own search strategies. | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md new file mode 100644 index 0000000000000..c06b8b00806bf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) > [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) + +## ISearchSetup.registerSearchStrategy property + +Extension point exposed for other plugins to register their own search strategies. + +Signature: + +```typescript +registerSearchStrategy: TRegisterSearchStrategy; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md new file mode 100644 index 0000000000000..0ba4bf578d6cc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) + +## ISearchStart.getSearchStrategy property + +Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. + +Signature: + +```typescript +getSearchStrategy: TGetSearchStrategy; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md new file mode 100644 index 0000000000000..abe72396f61e1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) + +## ISearchStart interface + +Signature: + +```typescript +export interface ISearchStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | TGetSearchStrategy | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md new file mode 100644 index 0000000000000..c1e0c3d9f2330 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) > [cancel](./kibana-plugin-plugins-data-server.isearchstrategy.cancel.md) + +## ISearchStrategy.cancel property + +Signature: + +```typescript +cancel?: ISearchCancel; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md new file mode 100644 index 0000000000000..167c6ab6e5a16 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) + +## ISearchStrategy interface + +Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. + +Signature: + +```typescript +export interface ISearchStrategy +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-data-server.isearchstrategy.cancel.md) | ISearchCancel<T> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | ISearch<T> | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md new file mode 100644 index 0000000000000..34a17ca87807a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) > [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) + +## ISearchStrategy.search property + +Signature: + +```typescript +search: ISearch; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 0efbe8ed4ed64..f492ba2843a69 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -39,10 +39,12 @@ | [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Use data plugin interface instead | | [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) | | -| [IRequestTypesMap](./kibana-plugin-plugins-data-server.irequesttypesmap.md) | | -| [IResponseTypesMap](./kibana-plugin-plugins-data-server.iresponsetypesmap.md) | | -| [ISearchContext](./kibana-plugin-plugins-data-server.isearchcontext.md) | | +| [IRequestTypesMap](./kibana-plugin-plugins-data-server.irequesttypesmap.md) | The map of search strategy IDs to the corresponding request type definitions. | +| [IResponseTypesMap](./kibana-plugin-plugins-data-server.iresponsetypesmap.md) | The map of search strategy IDs to the corresponding response type definitions. | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | +| [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | +| [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | +| [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | | [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | @@ -73,5 +75,5 @@ | [ISearch](./kibana-plugin-plugins-data-server.isearch.md) | | | [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | -| [TSearchStrategyProvider](./kibana-plugin-plugins-data-server.tsearchstrategyprovider.md) | Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. | +| [TStrategyTypes](./kibana-plugin-plugins-data-server.tstrategytypes.md) | Contains all known strategy type identifiers that will be used to map to request and response shapes. Plugins that wish to add their own custom search strategies should extend this type via:const MY\_STRATEGY = 'MY\_STRATEGY';declare module 'src/plugins/search/server' { export interface IRequestTypesMap { \[MY\_STRATEGY\]: IMySearchRequest; }export interface IResponseTypesMap { \[MY\_STRATEGY\]: IMySearchResponse } } | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index bd617990a00a2..13c69d6bf7548 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -7,11 +7,11 @@ Signature: ```typescript -setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { +setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { + search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; - search: ISearchSetup; }; ``` @@ -19,15 +19,15 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup | | +| core | CoreSetup<object, DataPluginStart> | | | { usageCollection } | DataPluginSetupDependencies | | Returns: `{ + search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; - search: ISearchSetup; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 2a30cd3e68158..2c7a833ab641b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -8,8 +8,9 @@ ```typescript start(core: CoreStart): { + search: ISearchStart; fieldFormats: { - fieldFormatServiceFactory: (uiSettings: import("kibana/server").IUiSettingsClient) => Promise; + fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; }; ``` @@ -23,8 +24,9 @@ start(core: CoreStart): { Returns: `{ + search: ISearchStart; fieldFormats: { - fieldFormatServiceFactory: (uiSettings: import("kibana/server").IUiSettingsClient) => Promise; + fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md index b7d6a7e8a83fd..1377d82123d41 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md @@ -15,4 +15,5 @@ export interface DataPluginStart | Property | Type | Description | | --- | --- | --- | | [fieldFormats](./kibana-plugin-plugins-data-server.pluginstart.fieldformats.md) | FieldFormatsStart | | +| [search](./kibana-plugin-plugins-data-server.pluginstart.search.md) | ISearchStart | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.search.md new file mode 100644 index 0000000000000..3144d8c40b780 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.search.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) > [search](./kibana-plugin-plugins-data-server.pluginstart.search.md) + +## PluginStart.search property + +Signature: + +```typescript +search: ISearchStart; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tsearchstrategyprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tsearchstrategyprovider.md deleted file mode 100644 index f528f48a68f72..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tsearchstrategyprovider.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TSearchStrategyProvider](./kibana-plugin-plugins-data-server.tsearchstrategyprovider.md) - -## TSearchStrategyProvider type - -Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. - -Signature: - -```typescript -export declare type TSearchStrategyProvider = (context: ISearchContext, caller: APICaller, search: ISearchGeneric) => ISearchStrategy; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tstrategytypes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tstrategytypes.md new file mode 100644 index 0000000000000..443d8d1b424d0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tstrategytypes.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TStrategyTypes](./kibana-plugin-plugins-data-server.tstrategytypes.md) + +## TStrategyTypes type + +Contains all known strategy type identifiers that will be used to map to request and response shapes. Plugins that wish to add their own custom search strategies should extend this type via: + +const MY\_STRATEGY = 'MY\_STRATEGY'; + +declare module 'src/plugins/search/server' { export interface IRequestTypesMap { \[MY\_STRATEGY\]: IMySearchRequest; } + +export interface IResponseTypesMap { \[MY\_STRATEGY\]: IMySearchResponse } } + +Signature: + +```typescript +export declare type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; +``` diff --git a/examples/demo_search/server/async_demo_search_strategy.ts b/examples/demo_search/server/async_demo_search_strategy.ts index 7ed5062acba48..2eda0f4d09e11 100644 --- a/examples/demo_search/server/async_demo_search_strategy.ts +++ b/examples/demo_search/server/async_demo_search_strategy.ts @@ -17,30 +17,32 @@ * under the License. */ -import { TSearchStrategyProvider } from '../../../src/plugins/data/server'; -import { ASYNC_DEMO_SEARCH_STRATEGY } from '../common'; - -function getFibonacciSequence(n = 0) { - const beginning = [0, 1].slice(0, n); - return Array(Math.max(0, n)) - .fill(null) - .reduce((sequence, value, i) => { - if (i < 2) return sequence; - return [...sequence, sequence[i - 1] + sequence[i - 2]]; - }, beginning); -} - -const generateId = (() => { - let id = 0; - return () => `${id++}`; -})(); - -const loadedMap = new Map(); -const totalMap = new Map(); - -export const asyncDemoSearchStrategyProvider: TSearchStrategyProvider = () => { +import { ISearchStrategy } from '../../../src/plugins/data/server'; +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../common'; + +export const asyncDemoSearchStrategyProvider = (): ISearchStrategy< + typeof ASYNC_DEMO_SEARCH_STRATEGY +> => { + function getFibonacciSequence(n = 0) { + const beginning = [0, 1].slice(0, n); + return Array(Math.max(0, n)) + .fill(null) + .reduce((sequence, value, i) => { + if (i < 2) return sequence; + return [...sequence, sequence[i - 1] + sequence[i - 2]]; + }, beginning); + } + + const generateId = (() => { + let id = 0; + return () => `${id++}`; + })(); + + const loadedMap = new Map(); + const totalMap = new Map(); + return { - search: async (request) => { + search: async (context, request: IAsyncDemoRequest) => { const id = request.id ?? generateId(); const loaded = (loadedMap.get(id) ?? 0) + 1; @@ -52,7 +54,7 @@ export const asyncDemoSearchStrategyProvider: TSearchStrategyProvider { + cancel: async (context, id) => { loadedMap.delete(id); totalMap.delete(id); }, diff --git a/examples/demo_search/server/demo_search_strategy.ts b/examples/demo_search/server/demo_search_strategy.ts index a1fd0e45dbc8e..36280ad22e83c 100644 --- a/examples/demo_search/server/demo_search_strategy.ts +++ b/examples/demo_search/server/demo_search_strategy.ts @@ -17,12 +17,12 @@ * under the License. */ -import { TSearchStrategyProvider } from '../../../src/plugins/data/server'; -import { DEMO_SEARCH_STRATEGY } from '../common'; +import { ISearchStrategy } from '../../../src/plugins/data/server'; +import { DEMO_SEARCH_STRATEGY, IDemoRequest } from '../common'; -export const demoSearchStrategyProvider: TSearchStrategyProvider = () => { +export const demoSearchStrategyProvider = (): ISearchStrategy => { return { - search: (request) => { + search: (context, request: IDemoRequest) => { return Promise.resolve({ greeting: request.mood === 'happy' diff --git a/examples/demo_search/server/index.ts b/examples/demo_search/server/index.ts index 6289b684b2b1e..368575b705c90 100644 --- a/examples/demo_search/server/index.ts +++ b/examples/demo_search/server/index.ts @@ -17,9 +17,6 @@ * under the License. */ -import { PluginInitializerContext, PluginInitializer } from 'kibana/server'; import { DemoDataPlugin } from './plugin'; -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new DemoDataPlugin(initializerContext); +export const plugin = () => new DemoDataPlugin(); diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts index 49fbae43e3aa2..8a72b5007f988 100644 --- a/examples/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; +import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; import { demoSearchStrategyProvider } from './demo_search_strategy'; import { @@ -56,18 +56,13 @@ declare module '../../../src/plugins/data/server' { } export class DemoDataPlugin implements Plugin { - constructor(private initializerContext: PluginInitializerContext) {} + constructor() {} public setup(core: CoreSetup, deps: IDemoSearchExplorerDeps) { - deps.data.search.registerSearchStrategyProvider( - this.initializerContext.opaqueId, - DEMO_SEARCH_STRATEGY, - demoSearchStrategyProvider - ); - deps.data.search.registerSearchStrategyProvider( - this.initializerContext.opaqueId, + deps.data.search.registerSearchStrategy(DEMO_SEARCH_STRATEGY, demoSearchStrategyProvider()); + deps.data.search.registerSearchStrategy( ASYNC_DEMO_SEARCH_STRATEGY, - asyncDemoSearchStrategyProvider + asyncDemoSearchStrategyProvider() ); } diff --git a/examples/search_explorer/public/search_api.tsx b/examples/search_explorer/public/search_api.tsx index c77ec725c6890..4a0e9afd20c85 100644 --- a/examples/search_explorer/public/search_api.tsx +++ b/examples/search_explorer/public/search_api.tsx @@ -23,11 +23,6 @@ import { GuideSection } from './guide_section'; import publicSearch from '!!raw-loader!./../../../src/plugins/data/public/search/i_search'; // @ts-ignore import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/search_service'; - -// @ts-ignore -import serverSetupContract from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_setup'; -// @ts-ignore -import serverSearch from '!!raw-loader!./../../../src/plugins/data/server/search/i_search'; // @ts-ignore import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/search_service'; @@ -54,14 +49,6 @@ export const SearchApiPage = () => ( description: 'search_service.ts', snippet: serverPlugin, }, - { - description: `i_search_setup.ts`, - snippet: serverSetupContract, - }, - { - description: 'i_search', - snippet: serverSearch, - }, ], }, ]} diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/field_formats/mocks.ts similarity index 80% rename from src/plugins/data/server/search/i_route_handler_search_context.ts rename to src/plugins/data/server/field_formats/mocks.ts index 9888c774ea104..ecfa33c86cf16 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/field_formats/mocks.ts @@ -17,9 +17,14 @@ * under the License. */ -import { ISearchGeneric, ISearchCancelGeneric } from './i_search'; +export function createFieldFormatsSetupMock() { + return { + register: jest.fn(), + }; +} -export interface IRouteHandlerSearchContext { - search: ISearchGeneric; - cancel: ISearchCancelGeneric; +export function createFieldFormatsStartMock() { + return { + fieldFormatServiceFactory: jest.fn(), + }; } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 831d23864d228..16ac59e300237 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -172,8 +172,10 @@ export { ISearchOptions, IRequestTypesMap, IResponseTypesMap, - ISearchContext, - TSearchStrategyProvider, + ISearchSetup, + ISearchStart, + TStrategyTypes, + ISearchStrategy, getDefaultSearchParams, getTotalLoaded, } from './search'; diff --git a/src/plugins/data/server/search/i_search_context.ts b/src/plugins/data/server/mocks.ts similarity index 60% rename from src/plugins/data/server/search/i_search_context.ts rename to src/plugins/data/server/mocks.ts index 9d9de055d994f..e2f2298234054 100644 --- a/src/plugins/data/server/search/i_search_context.ts +++ b/src/plugins/data/server/mocks.ts @@ -17,10 +17,24 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { CoreSetup, SharedGlobalConfig } from '../../../../core/server'; +import { createSearchSetupMock, createSearchStartMock } from './search/mocks'; +import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks'; -export interface ISearchContext { - core: CoreSetup; - config$: Observable; +function createSetupContract() { + return { + search: createSearchSetupMock(), + fieldFormats: createFieldFormatsSetupMock(), + }; } + +function createStartContract() { + return { + search: createSearchStartMock(), + fieldFormats: createFieldFormatsStartMock(), + }; +} + +export const dataPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 8c9d0df2ed894..0edce458f1c6b 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; import { ConfigSchema } from '../config'; import { IndexPatternsService } from './index_patterns'; -import { ISearchSetup } from './search'; +import { ISearchSetup, ISearchStart } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; import { ScriptsService } from './scripts'; @@ -36,6 +36,7 @@ export interface DataPluginSetup { } export interface DataPluginStart { + search: ISearchStart; fieldFormats: FieldFormatsStart; } @@ -59,7 +60,10 @@ export class DataServerPlugin implements Plugin, + { usageCollection }: DataPluginSetupDependencies + ) { this.indexPatterns.setup(core); this.scriptsService.setup(core); this.queryService.setup(core); @@ -69,13 +73,14 @@ export class DataServerPlugin implements Plugin Promise.resolve({ total: 100, loaded: 0 })); -const mockDefaultSearchStrategyProvider = jest.fn(() => - Promise.resolve({ - search: mockDefaultSearch, - }) -); -const mockStrategies: TSearchStrategiesMap = { - [DEFAULT_SEARCH_STRATEGY]: mockDefaultSearchStrategyProvider, -}; - -describe('createApi', () => { - let api: IRouteHandlerSearchContext; - - beforeEach(() => { - api = createApi({ - caller: jest.fn(), - searchStrategies: mockStrategies, - }); - mockDefaultSearchStrategyProvider.mockClear(); - }); - - it('should default to DEFAULT_SEARCH_STRATEGY if none is provided', async () => { - await api.search({ - params: {}, - }); - expect(mockDefaultSearchStrategyProvider).toBeCalled(); - expect(mockDefaultSearch).toBeCalled(); - }); - - it('should throw if no provider is found for the given name', () => { - expect(api.search({}, {}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( - `"No strategy found for noneByThisName"` - ); - }); - - it('logs the response if `debug` is set to `true`', async () => { - const spy = jest.spyOn(console, 'log'); - await api.search({ params: {} }); - - expect(spy).not.toBeCalled(); - - await api.search({ debug: true, params: {} }); - - expect(spy).toBeCalled(); - }); -}); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts deleted file mode 100644 index 00665b21f2ba7..0000000000000 --- a/src/plugins/data/server/search/create_api.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { APICaller } from 'kibana/server'; -import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; -import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; -import { TSearchStrategiesMap } from './i_search_strategy'; - -export function createApi({ - caller, - searchStrategies, -}: { - searchStrategies: TSearchStrategiesMap; - caller: APICaller; -}) { - const api: IRouteHandlerSearchContext = { - search: async (request, options, strategyName) => { - if (request.debug) { - // eslint-disable-next-line - console.log(JSON.stringify(request, null, 2)); - } - const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; - const strategyProvider = searchStrategies[name]; - if (!strategyProvider) { - throw new Error(`No strategy found for ${strategyName}`); - } - // Give providers access to other search strategies by injecting this function - const strategy = await strategyProvider(caller, api.search); - return strategy.search(request, options); - }, - cancel: async (id, strategyName) => { - const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; - const strategyProvider = searchStrategies[name]; - if (!strategyProvider) { - throw new Error(`No strategy found for ${strategyName}`); - } - const strategy = await strategyProvider(caller, api.search); - return strategy.cancel && strategy.cancel(id); - }, - }; - return api; -} diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index c4b8119f9e095..1155a5491e8f3 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -17,11 +17,11 @@ * under the License. */ -import { coreMock, pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; +import { RequestHandlerContext } from '../../../../../core/server'; +import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { esSearchStrategyProvider } from './es_search_strategy'; describe('ES search strategy', () => { - const mockCoreSetup = coreMock.createSetup(); const mockApiCaller = jest.fn().mockResolvedValue({ _shards: { total: 10, @@ -30,39 +30,26 @@ describe('ES search strategy', () => { successful: 7, }, }); - const mockSearch = jest.fn(); + const mockContext = { + core: { elasticsearch: { legacy: { client: { callAsCurrentUser: mockApiCaller } } } }, + }; const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { mockApiCaller.mockClear(); - mockSearch.mockClear(); }); - it('returns a strategy with `search`', () => { - const esSearch = esSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + it('returns a strategy with `search`', async () => { + const esSearch = await esSearchStrategyProvider(mockConfig$); expect(typeof esSearch.search).toBe('function'); }); it('calls the API caller with the params with defaults', async () => { const params = { index: 'logstash-*' }; - const esSearch = esSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await esSearchStrategyProvider(mockConfig$); - await esSearch.search({ params }); + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); @@ -76,16 +63,9 @@ describe('ES search strategy', () => { it('calls the API caller with overridden defaults', async () => { const params = { index: 'logstash-*', ignoreUnavailable: false, timeout: '1000ms' }; - const esSearch = esSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await esSearchStrategyProvider(mockConfig$); - await esSearch.search({ params }); + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); @@ -97,16 +77,11 @@ describe('ES search strategy', () => { it('returns total, loaded, and raw response', async () => { const params = { index: 'logstash-*' }; - const esSearch = esSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await esSearchStrategyProvider(mockConfig$); - const response = await esSearch.search({ params }); + const response = await esSearch.search((mockContext as unknown) as RequestHandlerContext, { + params, + }); expect(response).toHaveProperty('total'); expect(response).toHaveProperty('loaded'); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 47cad7aa6b4d7..db08ddf920818 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -17,19 +17,18 @@ * under the License. */ import { first } from 'rxjs/operators'; -import { APICaller } from 'kibana/server'; +import { RequestHandlerContext, SharedGlobalConfig } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; +import { Observable } from 'rxjs'; import { ES_SEARCH_STRATEGY } from '../../../common/search'; -import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; -import { getDefaultSearchParams, getTotalLoaded, ISearchContext } from '..'; +import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; -export const esSearchStrategyProvider: TSearchStrategyProvider = ( - context: ISearchContext, - caller: APICaller +export const esSearchStrategyProvider = ( + config$: Observable ): ISearchStrategy => { return { - search: async (request, options) => { - const config = await context.config$.pipe(first()).toPromise(); + search: async (context: RequestHandlerContext, request, options) => { + const config = await config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); // Only default index pattern type is supported here. @@ -42,7 +41,12 @@ export const esSearchStrategyProvider: TSearchStrategyProvider; + + const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + params, + options + )) as SearchResponse; // The above query will either complete or timeout and throw an error. // There is no progress indication on this api. diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts deleted file mode 100644 index fa4aa72ac7287..0000000000000 --- a/src/plugins/data/server/search/i_search.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; -import { TStrategyTypes } from './strategy_types'; -import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; -import { IEsSearchRequest } from './es_search'; - -export interface ISearchOptions { - signal?: AbortSignal; -} - -export interface IRequestTypesMap { - [ES_SEARCH_STRATEGY]: IEsSearchRequest; - [key: string]: IKibanaSearchRequest; -} - -export interface IResponseTypesMap { - [ES_SEARCH_STRATEGY]: IEsSearchResponse; - [key: string]: IKibanaSearchResponse; -} - -export type ISearchGeneric = ( - request: IRequestTypesMap[T], - options?: ISearchOptions, - strategy?: T -) => Promise; - -export type ISearchCancelGeneric = ( - id: string, - strategy?: T -) => Promise; - -export type ISearch = ( - request: IRequestTypesMap[T], - options?: ISearchOptions -) => Promise; - -export type ISearchCancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_setup.ts b/src/plugins/data/server/search/i_search_setup.ts deleted file mode 100644 index e4a4d50141201..0000000000000 --- a/src/plugins/data/server/search/i_search_setup.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IContextProvider } from 'kibana/server'; -import { ISearchContext } from './i_search_context'; -import { TRegisterSearchStrategyProvider, TSearchStrategyProvider } from './i_search_strategy'; - -/** - * The setup contract exposed by the Search plugin exposes the search strategy extension - * point. - */ -export interface ISearchSetup { - registerSearchStrategyContext: ( - pluginId: symbol, - strategyName: TContextName, - provider: IContextProvider, TContextName> - ) => void; - - /** - * Extension point exposed for other plugins to register their own search - * strategies. - */ - registerSearchStrategyProvider: TRegisterSearchStrategyProvider; -} diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts deleted file mode 100644 index 9b405034f883f..0000000000000 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { APICaller } from 'kibana/server'; -import { ISearch, ISearchCancel, ISearchGeneric } from './i_search'; -import { TStrategyTypes } from './strategy_types'; -import { ISearchContext } from './i_search_context'; - -/** - * Search strategy interface contains a search method that takes in - * a request and returns a promise that resolves to a response. - */ -export interface ISearchStrategy { - search: ISearch; - cancel?: ISearchCancel; -} - -/** - * Search strategy provider creates an instance of a search strategy with the request - * handler context bound to it. This way every search strategy can use - * whatever information they require from the request context. - */ -export type TSearchStrategyProviderEnhanced = ( - caller: APICaller, - search: ISearchGeneric -) => Promise>; - -/** - * Search strategy provider creates an instance of a search strategy with the request - * handler context bound to it. This way every search strategy can use - * whatever information they require from the request context. - */ -export type TSearchStrategyProvider = ( - context: ISearchContext, - caller: APICaller, - search: ISearchGeneric -) => ISearchStrategy; - -/** - * Extension point exposed for other plugins to register their own search - * strategies. - */ -export type TRegisterSearchStrategyProvider = ( - opaqueId: symbol, - name: T, - searchStrategyProvider: TSearchStrategyProvider -) => void; - -export type TSearchStrategiesMap = { - [K in TStrategyTypes]?: TSearchStrategyProviderEnhanced; -}; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index e08eba1cad831..882f56e83d4ca 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -17,20 +17,16 @@ * under the License. */ -export { ISearchSetup } from './i_search_setup'; - -export { ISearchContext } from './i_search_context'; - export { ISearch, ISearchCancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap, -} from './i_search'; - -export { TStrategyTypes } from './strategy_types'; - -export { TSearchStrategyProvider } from './i_search_strategy'; + ISearchSetup, + ISearchStart, + TStrategyTypes, + ISearchStrategy, +} from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 136e7a1d580c9..0aab466a9a0d9 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,10 +17,14 @@ * under the License. */ -export const searchSetupMock = { - registerSearchStrategyContext: jest.fn(), - registerSearchStrategyProvider: jest.fn(), - __LEGACY: { - search: jest.fn(), - }, -}; +export function createSearchSetupMock() { + return { + registerSearchStrategy: jest.fn(), + }; +} + +export function createSearchStartMock() { + return { + getSearchStrategy: jest.fn(), + }; +} diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index f5e6507d977cd..4ef67de93e454 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -17,36 +17,26 @@ * under the License. */ -import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { CoreSetup, RequestHandlerContext } from '../../../../../src/core/server'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { registerSearchRoute } from './routes'; -import { IRouter, ScopedClusterClient, RequestHandlerContext } from 'kibana/server'; +import { DataPluginStart } from '../plugin'; +import { dataPluginMock } from '../mocks'; describe('Search service', () => { - let routerMock: jest.Mocked; + let mockDataStart: MockedKeys; + let mockCoreSetup: MockedKeys>; beforeEach(() => { - routerMock = httpServiceMock.createRouter(); - }); - - it('registers a post route', async () => { - registerSearchRoute(routerMock); - expect(routerMock.post).toBeCalled(); + mockDataStart = dataPluginMock.createStartContract(); + mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart }); }); it('handler calls context.search.search with the given request and strategy', async () => { const mockSearch = jest.fn().mockResolvedValue('yay'); - const mockContext = { - core: { - elasticsearch: { - legacy: { - client: {} as ScopedClusterClient, - }, - }, - }, - search: { - search: mockSearch, - }, - }; + mockDataStart.search.getSearchStrategy.mockReturnValueOnce({ search: mockSearch }); + + const mockContext = {}; const mockBody = { params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ @@ -55,13 +45,15 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(routerMock); - const handler = routerMock.post.mock.calls[0][1]; + registerSearchRoute(mockCoreSetup); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + expect(mockDataStart.search.getSearchStrategy.mock.calls[0][0]).toBe(mockParams.strategy); expect(mockSearch).toBeCalled(); - expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][1]).toStrictEqual(mockBody); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); @@ -73,18 +65,9 @@ describe('Search service', () => { error: 'oops', }, }); - const mockContext = { - core: { - elasticsearch: { - legacy: { - client: {} as ScopedClusterClient, - }, - }, - }, - search: { - search: mockSearch, - }, - }; + mockDataStart.search.getSearchStrategy.mockReturnValueOnce({ search: mockSearch }); + + const mockContext = {}; const mockBody = { params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ @@ -93,13 +76,15 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(routerMock); - const handler = routerMock.post.mock.calls[0][1]; + registerSearchRoute(mockCoreSetup); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + expect(mockDataStart.search.getSearchStrategy.mock.calls[0][0]).toBe(mockParams.strategy); expect(mockSearch).toBeCalled(); - expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][1]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index b90d7d4ff80ce..7b6c045b0908c 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -18,10 +18,13 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '../../../../core/server'; +import { CoreSetup } from '../../../../core/server'; import { getRequestAbortedSignal } from '../lib'; +import { DataPluginStart } from '../plugin'; + +export function registerSearchRoute(core: CoreSetup): void { + const router = core.http.createRouter(); -export function registerSearchRoute(router: IRouter): void { router.post( { path: '/internal/search/{strategy}', @@ -38,8 +41,11 @@ export function registerSearchRoute(router: IRouter): void { const { strategy } = request.params; const signal = getRequestAbortedSignal(request.events.aborted$); + const [, , selfStart] = await core.getStartServices(); + const searchStrategy = selfStart.search.getSearchStrategy(strategy); + try { - const response = await context.search!.search(searchRequest, { signal }, strategy); + const response = await searchStrategy.search(context, searchRequest, { signal }); return res.ok({ body: response }); } catch (err) { return res.customError({ @@ -69,8 +75,13 @@ export function registerSearchRoute(router: IRouter): void { }, async (context, request, res) => { const { strategy, id } = request.params; + + const [, , selfStart] = await core.getStartServices(); + const searchStrategy = selfStart.search.getSearchStrategy(strategy); + if (!searchStrategy.cancel) return res.ok(); + try { - await context.search!.cancel(id, strategy); + await searchStrategy.cancel(context, id); return res.ok(); } catch (err) { return res.customError({ diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index fa659756c1273..25143fa09e6bf 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -21,27 +21,28 @@ import { coreMock } from '../../../../core/server/mocks'; import { SearchService } from './search_service'; import { CoreSetup } from '../../../../core/server'; - -const mockSearchApi = { search: jest.fn() }; -jest.mock('./create_api', () => ({ - createApi: () => mockSearchApi, -})); +import { DataPluginStart } from '../plugin'; describe('Search service', () => { let plugin: SearchService; - let mockCoreSetup: MockedKeys; + let mockCoreSetup: MockedKeys>; beforeEach(() => { plugin = new SearchService(coreMock.createPluginInitializerContext({})); mockCoreSetup = coreMock.createSetup(); - mockSearchApi.search.mockClear(); }); describe('setup()', () => { it('exposes proper contract', async () => { const setup = plugin.setup(mockCoreSetup); - expect(setup).toHaveProperty('registerSearchStrategyContext'); - expect(setup).toHaveProperty('registerSearchStrategyProvider'); + expect(setup).toHaveProperty('registerSearchStrategy'); + }); + }); + + describe('start()', () => { + it('exposes proper contract', async () => { + const setup = plugin.start(); + expect(setup).toHaveProperty('getSearchStrategy'); }); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 1c267c32ebc37..df809b425eb9e 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,82 +17,52 @@ * under the License. */ +import { Plugin, PluginInitializerContext, CoreSetup } from '../../../../core/server'; import { - PluginInitializerContext, - Plugin, - CoreSetup, - IContextContainer, -} from '../../../../core/server'; -import { registerSearchRoute } from './routes'; -import { ISearchSetup } from './i_search_setup'; -import { createApi } from './create_api'; -import { + ISearchSetup, + ISearchStart, TSearchStrategiesMap, - TSearchStrategyProvider, - TRegisterSearchStrategyProvider, -} from './i_search_strategy'; -import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; + TRegisterSearchStrategy, + TGetSearchStrategy, +} from './types'; +import { registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; - import { searchSavedObjectType } from '../saved_objects'; +import { DataPluginStart } from '../plugin'; -declare module 'kibana/server' { - interface RequestHandlerContext { - search?: IRouteHandlerSearchContext; - } -} - -export class SearchService implements Plugin { +export class SearchService implements Plugin { private searchStrategies: TSearchStrategiesMap = {}; - private contextContainer?: IContextContainer>; - constructor(private initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup): ISearchSetup { - const router = core.http.createRouter(); - registerSearchRoute(router); - - this.contextContainer = core.context.createContextContainer(); - + public setup(core: CoreSetup): ISearchSetup { core.savedObjects.registerType(searchSavedObjectType); - core.http.registerRouteHandlerContext<'search'>('search', (context) => { - return createApi({ - caller: context.core.elasticsearch.legacy.client.callAsCurrentUser, - searchStrategies: this.searchStrategies, - }); - }); - - const registerSearchStrategyProvider: TRegisterSearchStrategyProvider = ( - plugin, - name, - strategyProvider - ) => { - this.searchStrategies[name] = this.contextContainer!.createHandler(plugin, strategyProvider); - }; - - const api: ISearchSetup = { - registerSearchStrategyContext: this.contextContainer!.registerContext, - registerSearchStrategyProvider, - }; - - api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core); - api.registerSearchStrategyContext( - this.initializerContext.opaqueId, - 'config$', - () => this.initializerContext.config.legacy.globalConfig$ - ); - - api.registerSearchStrategyProvider( - this.initializerContext.opaqueId, + this.registerSearchStrategy( ES_SEARCH_STRATEGY, - esSearchStrategyProvider + esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$) ); - return api; + registerSearchRoute(core); + + return { registerSearchStrategy: this.registerSearchStrategy }; + } + + public start(): ISearchStart { + return { getSearchStrategy: this.getSearchStrategy }; } - public start() {} public stop() {} + + private registerSearchStrategy: TRegisterSearchStrategy = (name, strategy) => { + this.searchStrategies[name] = strategy; + }; + + private getSearchStrategy: TGetSearchStrategy = (name) => { + const strategy = this.searchStrategies[name]; + if (!strategy) { + throw new Error(`Search strategy ${name} not found`); + } + return strategy; + }; } diff --git a/src/plugins/data/server/search/strategy_types.ts b/src/plugins/data/server/search/strategy_types.ts deleted file mode 100644 index 252e0c8f9e6c9..0000000000000 --- a/src/plugins/data/server/search/strategy_types.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ES_SEARCH_STRATEGY } from '../../common/search/es_search'; - -/** - * Contains all known strategy type identifiers that will be used to map to - * request and response shapes. Plugins that wish to add their own custom search - * strategies should extend this type via: - * - * const MY_STRATEGY = 'MY_STRATEGY'; - * - * declare module 'src/plugins/search/server' { - * export interface IRequestTypesMap { - * [MY_STRATEGY]: IMySearchRequest; - * } - * - * export interface IResponseTypesMap { - * [MY_STRATEGY]: IMySearchResponse - * } - * } - */ -export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts new file mode 100644 index 0000000000000..dea325cc063bb --- /dev/null +++ b/src/plugins/data/server/search/types.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RequestHandlerContext } from '../../../../core/server'; +import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; +import { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from './es_search'; + +export interface ISearchSetup { + /** + * Extension point exposed for other plugins to register their own search + * strategies. + */ + registerSearchStrategy: TRegisterSearchStrategy; +} + +export interface ISearchStart { + /** + * Get other registered search strategies. For example, if a new strategy needs to use the + * already-registered ES search strategy, it can use this function to accomplish that. + */ + getSearchStrategy: TGetSearchStrategy; +} + +export interface ISearchOptions { + /** + * An `AbortSignal` that allows the caller of `search` to abort a search request. + */ + signal?: AbortSignal; +} + +/** + * Contains all known strategy type identifiers that will be used to map to + * request and response shapes. Plugins that wish to add their own custom search + * strategies should extend this type via: + * + * const MY_STRATEGY = 'MY_STRATEGY'; + * + * declare module 'src/plugins/search/server' { + * export interface IRequestTypesMap { + * [MY_STRATEGY]: IMySearchRequest; + * } + * + * export interface IResponseTypesMap { + * [MY_STRATEGY]: IMySearchResponse + * } + * } + */ +export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; + +/** + * The map of search strategy IDs to the corresponding request type definitions. + */ +export interface IRequestTypesMap { + [ES_SEARCH_STRATEGY]: IEsSearchRequest; + [key: string]: IKibanaSearchRequest; +} + +/** + * The map of search strategy IDs to the corresponding response type definitions. + */ +export interface IResponseTypesMap { + [ES_SEARCH_STRATEGY]: IEsSearchResponse; + [key: string]: IKibanaSearchResponse; +} + +export type ISearch = ( + context: RequestHandlerContext, + request: IRequestTypesMap[T], + options?: ISearchOptions +) => Promise; + +export type ISearchCancel = ( + context: RequestHandlerContext, + id: string +) => Promise; + +/** + * Search strategy interface contains a search method that takes in a request and returns a promise + * that resolves to a response. + */ +export interface ISearchStrategy { + search: ISearch; + cancel?: ISearchCancel; +} + +export type TRegisterSearchStrategy = ( + name: T, + searchStrategy: ISearchStrategy +) => void; + +export type TGetSearchStrategy = (name: T) => ISearchStrategy; + +export type TSearchStrategiesMap = { + [K in TStrategyTypes]?: ISearchStrategy; +}; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2d4185415b9d5..d825153a7aa12 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -48,7 +48,6 @@ import { GetResponse } from 'elasticsearch'; import { GetScriptParams } from 'elasticsearch'; import { GetSourceParams } from 'elasticsearch'; import { GetTemplateParams } from 'elasticsearch'; -import { IContextProvider as IContextProvider_2 } from 'kibana/server'; import { IncomingHttpHeaders } from 'http'; import { IndexDocumentParams } from 'elasticsearch'; import { IndicesAnalyzeParams } from 'elasticsearch'; @@ -490,7 +489,7 @@ export class IndexPatternsFetcher { // Warning: (ae-missing-release-tag) "IRequestTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface IRequestTypesMap { // Warning: (ae-forgotten-export) The symbol "IKibanaSearchRequest" needs to be exported by the entry point index.d.ts // @@ -505,7 +504,7 @@ export interface IRequestTypesMap { // Warning: (ae-missing-release-tag) "IResponseTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface IResponseTypesMap { // Warning: (ae-forgotten-export) The symbol "IKibanaSearchResponse" needs to be exported by the entry point index.d.ts // @@ -517,35 +516,48 @@ export interface IResponseTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchResponse; } -// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RequestHandlerContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ISearch = (request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; +export type ISearch = (context: RequestHandlerContext, request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; // Warning: (ae-missing-release-tag) "ISearchCancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ISearchCancel = (id: string) => Promise; +export type ISearchCancel = (context: RequestHandlerContext, id: string) => Promise; -// Warning: (ae-missing-release-tag) "ISearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ISearchContext { - // (undocumented) - config$: Observable; - // Warning: (ae-forgotten-export) The symbol "CoreSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: CoreSetup; +export interface ISearchOptions { + signal?: AbortSignal; } -// Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "ISearchSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ISearchOptions { +export interface ISearchSetup { + // Warning: (ae-forgotten-export) The symbol "TRegisterSearchStrategy" needs to be exported by the entry point index.d.ts + registerSearchStrategy: TRegisterSearchStrategy; +} + +// Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISearchStart { + // Warning: (ae-forgotten-export) The symbol "TGetSearchStrategy" needs to be exported by the entry point index.d.ts + getSearchStrategy: TGetSearchStrategy; +} + +// Warning: (ae-missing-release-tag) "ISearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface ISearchStrategy { // (undocumented) - signal?: AbortSignal; + cancel?: ISearchCancel; + // (undocumented) + search: ISearch; } // @public (undocumented) @@ -612,21 +624,23 @@ export function parseInterval(interval: string): moment.Duration | null; export class Plugin implements Plugin_2 { // Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts constructor(initializerContext: PluginInitializerContext); + // Warning: (ae-forgotten-export) The symbol "CoreSetup" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts // // (undocumented) - setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { + setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { + search: ISearchSetup; fieldFormats: { register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; - search: ISearchSetup; }; // Warning: (ae-forgotten-export) The symbol "CoreStart" needs to be exported by the entry point index.d.ts // // (undocumented) start(core: CoreStart): { + search: ISearchStart; fieldFormats: { - fieldFormatServiceFactory: (uiSettings: import("kibana/server").IUiSettingsClient) => Promise; + fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; }; // (undocumented) @@ -656,6 +670,8 @@ export interface PluginStart { // // (undocumented) fieldFormats: FieldFormatsStart; + // (undocumented) + search: ISearchStart; } // Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -713,12 +729,10 @@ export interface TimeRange { to: string; } -// Warning: (ae-forgotten-export) The symbol "ISearchGeneric" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ISearchStrategy" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "TSearchStrategyProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "TStrategyTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type TSearchStrategyProvider = (context: ISearchContext, caller: APICaller_2, search: ISearchGeneric) => ISearchStrategy; +export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; // Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -778,13 +792,12 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:191:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:66:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:190:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:193:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts index cb3a24375607e..c9274c867df83 100644 --- a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts @@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await supertest .get('/api/np-context-in-legacy') .expect(200) - .expect(JSON.stringify({ contexts: ['core', 'search', 'pluginA'] })); + .expect(JSON.stringify({ contexts: ['core', 'pluginA'] })); }); }); diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index a27a73431574b..4f6756231912c 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -22,10 +22,9 @@ export class EnhancedDataServerPlugin implements Plugin { - const mockCoreSetup = coreMock.createSetup(); const mockApiCaller = jest.fn(); - const mockSearch = jest.fn(); + const mockContext = { + core: { elasticsearch: { legacy: { client: { callAsCurrentUser: mockApiCaller } } } }, + }; const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { mockApiCaller.mockClear(); - mockSearch.mockClear(); }); - it('returns a strategy with `search`', () => { - const esSearch = enhancedEsSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + it('returns a strategy with `search`', async () => { + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); expect(typeof esSearch.search).toBe('function'); }); @@ -56,16 +50,9 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = enhancedEsSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); - await esSearch.search({ params }); + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); @@ -79,16 +66,9 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = enhancedEsSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); - await esSearch.search({ id: 'foo', params }); + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { id: 'foo', params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); @@ -102,16 +82,9 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'foo-程', body: {} }; - const esSearch = enhancedEsSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); - await esSearch.search({ params }); + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); @@ -124,16 +97,12 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockRollupResponse); const params = { index: 'foo-程', body: {} }; - const esSearch = enhancedEsSearchStrategyProvider( - { - core: mockCoreSetup, - config$: mockConfig$, - }, - mockApiCaller, - mockSearch - ); - - await esSearch.search({ indexType: 'rollup', params }); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { + indexType: 'rollup', + params, + }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 15f2ca10af7f7..9083ab24a4521 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -7,16 +7,16 @@ import { first } from 'rxjs/operators'; import { mapKeys, snakeCase } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { APICaller } from '../../../../../src/core/server'; +import { Observable } from 'rxjs'; +import { APICaller, SharedGlobalConfig } from '../../../../../src/core/server'; import { ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common'; import { - ISearchContext, - TSearchStrategyProvider, ISearch, ISearchOptions, ISearchCancel, getDefaultSearchParams, getTotalLoaded, + ISearchStrategy, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { shimHitsTotal } from './shim_hits_total'; @@ -28,15 +28,16 @@ export interface AsyncSearchResponse { response: SearchResponse; } -export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = ( - context: ISearchContext, - caller: APICaller -) => { +export const enhancedEsSearchStrategyProvider = ( + config$: Observable +): ISearchStrategy => { const search: ISearch = async ( + context, request: IEnhancedEsSearchRequest, options ) => { - const config = await context.config$.pipe(first()).toPromise(); + const config = await config$.pipe(first()).toPromise(); + const caller = context.core.elasticsearch.legacy.client.callAsCurrentUser; const defaultParams = getDefaultSearchParams(config); const params = { ...defaultParams, ...request.params }; @@ -45,10 +46,13 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = async (id) => { + const cancel: ISearchCancel = async (context, id) => { const method = 'DELETE'; const path = encodeURI(`/_async_search/${id}`); - await caller('transport.request', { method, path }); + await context.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', { + method, + path, + }); }; return { search, cancel }; From 5c327a12ddce4d902a583e21fc1d40f411e64641 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 16 Jun 2020 13:45:20 -0700 Subject: [PATCH 29/68] [ci/getCheckoutInfo] retry fetching upstream changes to calculate mergeBase (#69320) Co-authored-by: spalger --- vars/getCheckoutInfo.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/vars/getCheckoutInfo.groovy b/vars/getCheckoutInfo.groovy index 7a3a7a9d2eccc..32a7b054bfd15 100644 --- a/vars/getCheckoutInfo.groovy +++ b/vars/getCheckoutInfo.groovy @@ -22,10 +22,14 @@ def call(branchOverride) { ).trim() if (repoInfo.targetBranch) { - sh( - script: "git fetch origin ${repoInfo.targetBranch}", - label: "fetch latest from '${repoInfo.targetBranch}' at origin" - ) + // Try to clone fetch from Github up to 8 times, waiting 15 secs between attempts + retryWithDelay(8, 15) { + sh( + script: "git fetch origin ${repoInfo.targetBranch}", + label: "fetch latest from '${repoInfo.targetBranch}' at origin" + ) + } + repoInfo.mergeBase = sh( script: "git merge-base HEAD FETCH_HEAD", label: "determining merge point with '${repoInfo.targetBranch}' at origin", From 885a42ca15f0ac954d2d5fef749892939c5894e6 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 16 Jun 2020 14:59:26 -0600 Subject: [PATCH 30/68] [QA] [Code Coverage] Doc update (#69204) Co-authored-by: Elastic Machine --- .../kibana_code_coverage_index_mapping.md} | 10 +- .../ingest_code_coverage_readme.md | 15 +- ...qa_research_code_coverage_with_mapping.png | Bin 0 -> 829938 bytes .../qa_research_index_mapping.md | 193 ++++++++++++++++++ .../teams_scripted_field.painless | 0 5 files changed, 213 insertions(+), 5 deletions(-) rename src/dev/code_coverage/{ingest_coverage/index_mapping.md => docs/code_coverage_job/kibana_code_coverage_index_mapping.md} (97%) rename src/dev/code_coverage/{ingest_coverage => docs}/ingest_code_coverage_readme.md (52%) create mode 100644 src/dev/code_coverage/docs/qa_research_job/put_qa_research_code_coverage_with_mapping.png create mode 100644 src/dev/code_coverage/docs/qa_research_job/qa_research_index_mapping.md rename src/dev/code_coverage/{ingest_coverage => docs}/teams_scripted_field.painless (100%) diff --git a/src/dev/code_coverage/ingest_coverage/index_mapping.md b/src/dev/code_coverage/docs/code_coverage_job/kibana_code_coverage_index_mapping.md similarity index 97% rename from src/dev/code_coverage/ingest_coverage/index_mapping.md rename to src/dev/code_coverage/docs/code_coverage_job/kibana_code_coverage_index_mapping.md index c3b934eb35c5e..66654dd730518 100644 --- a/src/dev/code_coverage/ingest_coverage/index_mapping.md +++ b/src/dev/code_coverage/docs/code_coverage_job/kibana_code_coverage_index_mapping.md @@ -1,9 +1,12 @@ -# Create index mapping +# Index Mapping + +Execute the following in Kibana Dev Tools. -This is usually done in Kibana's dev tools ui. ``` - "mappings" : { +PUT /kibana_code_coverage +{ + "mappings" : { "properties" : { "@timestamp" : { "type" : "date" @@ -189,6 +192,7 @@ This is usually done in Kibana's dev tools ui. } } } +} ``` _The main portion of the above mapping, is the timestamp-date mapping._ \ No newline at end of file diff --git a/src/dev/code_coverage/ingest_coverage/ingest_code_coverage_readme.md b/src/dev/code_coverage/docs/ingest_code_coverage_readme.md similarity index 52% rename from src/dev/code_coverage/ingest_coverage/ingest_code_coverage_readme.md rename to src/dev/code_coverage/docs/ingest_code_coverage_readme.md index 0670780a7c03a..174f729180b77 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest_code_coverage_readme.md +++ b/src/dev/code_coverage/docs/ingest_code_coverage_readme.md @@ -1,7 +1,18 @@ -# Convert Code Coverage Json Summary and Send to ES +# Massage and Ingest Code Coverage Json Summary and Send to ES +## Currently, we have 4 indexes +### 2 for the Code Coverage Job +https://kibana-ci.elastic.co/job/elastic+kibana+code-coverage/ +1. kibana_code_coverage +2. kibana_total_code_coverage + +### 2 for the R & D Job +https://kibana-ci.elastic.co/job/elastic+kibana+qa-research/ +1. qa_research_code_coverage +2. qa_research_total_code_coverage + ## How it works It starts with this jenkins pipeline file: @@ -19,6 +30,6 @@ From there, an event stream is created, that massages the data to an output form ## Configuration There is really only one config step. -The index [mapping](src/dev/code_coverage/ingest_coverage/index_mapping.md) for one of +The index [mapping](./code_coverage_job/kibana_code_coverage_index_mapping.md) for one of of the indexes has to be manually created. Currently, we just add it using Kibana's Dev Tools. \ No newline at end of file diff --git a/src/dev/code_coverage/docs/qa_research_job/put_qa_research_code_coverage_with_mapping.png b/src/dev/code_coverage/docs/qa_research_job/put_qa_research_code_coverage_with_mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..c2e4ee4546dfcf08159c5a4a4ae3576e942d3ed1 GIT binary patch literal 829938 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 z

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%0DJ-z`D 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 literal 0 HcmV?d00001 diff --git a/src/dev/code_coverage/docs/qa_research_job/qa_research_index_mapping.md b/src/dev/code_coverage/docs/qa_research_job/qa_research_index_mapping.md new file mode 100644 index 0000000000000..746f81c761fcc --- /dev/null +++ b/src/dev/code_coverage/docs/qa_research_job/qa_research_index_mapping.md @@ -0,0 +1,193 @@ +``` +PUT /qa_research_code_coverage +{ + "mappings" : { + "properties" : { + "@timestamp" : { + "type" : "date" + }, + "BUILD_ID" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "branches" : { + "properties" : { + "covered" : { + "type" : "long" + }, + "pct" : { + "type" : "long" + }, + "skipped" : { + "type" : "long" + }, + "total" : { + "type" : "long" + } + } + }, + "ciRunUrl" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "coveredFilePath" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "functions" : { + "properties" : { + "covered" : { + "type" : "long" + }, + "pct" : { + "type" : "long" + }, + "skipped" : { + "type" : "long" + }, + "total" : { + "type" : "long" + } + } + }, + "isTotal" : { + "type" : "boolean" + }, + "jsonSummaryPath" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "lines" : { + "properties" : { + "covered" : { + "type" : "long" + }, + "pct" : { + "type" : "long" + }, + "skipped" : { + "type" : "long" + }, + "total" : { + "type" : "long" + } + } + }, + "path" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "statements" : { + "properties" : { + "covered" : { + "type" : "long" + }, + "pct" : { + "type" : "long" + }, + "skipped" : { + "type" : "long" + }, + "total" : { + "type" : "long" + } + } + }, + "staticSiteUrl" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "testRunnerType" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "vcs" : { + "properties" : { + "author" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "branch" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "commitMsg" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "sha" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "vcsUrl" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + } + } + } +} +``` + +Execute the above in Kibana Dev Tools, eg: ![Index Mapping Screenshot](./put_qa_research_code_coverage_with_mapping.png "QA Research Code Coverage Index Mapping") \ No newline at end of file diff --git a/src/dev/code_coverage/ingest_coverage/teams_scripted_field.painless b/src/dev/code_coverage/docs/teams_scripted_field.painless similarity index 100% rename from src/dev/code_coverage/ingest_coverage/teams_scripted_field.painless rename to src/dev/code_coverage/docs/teams_scripted_field.painless From beb0da1d58c609c86565ff23e8e541a056c39eb9 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 16 Jun 2020 17:03:59 -0400 Subject: [PATCH 31/68] [CI] Fix packer cache git branch reference value (#69207) --- .ci/packer_cache.sh | 58 ++----------------------------- .ci/packer_cache_for_branch.sh | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 56 deletions(-) create mode 100755 .ci/packer_cache_for_branch.sh diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index 11f9ccaeddb1e..e4b5e35e1e4a9 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -2,59 +2,5 @@ set -e -branch="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)" - -# run setup script that gives us node, yarn, and bootstraps the project -source src/dev/ci_setup/setup.sh; - -# download es snapshots -node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - -# download reporting browsers -(cd "x-pack" && yarn gulp prepare); - -# cache the chromedriver archive -chromedriverDistVersion="$(node -e "console.log(require('chromedriver').version)")" -chromedriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.chromedriver)")" -if [ -z "$chromedriverDistVersion" ] || [ -z "$chromedriverPkgVersion" ]; then - echo "UNABLE TO DETERMINE CHROMEDRIVER VERSIONS" - exit 1 -fi -mkdir -p .chromedriver -curl "https://chromedriver.storage.googleapis.com/$chromedriverDistVersion/chromedriver_linux64.zip" > .chromedriver/chromedriver.zip -echo "$chromedriverPkgVersion" > .chromedriver/pkgVersion - -# cache the geckodriver archive -geckodriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.geckodriver)")" -if [ -z "$geckodriverPkgVersion" ]; then - echo "UNABLE TO DETERMINE geckodriver VERSIONS" - exit 1 -fi -mkdir -p ".geckodriver" -cp "node_modules/geckodriver/geckodriver.tar.gz" .geckodriver/geckodriver.tar.gz -echo "$geckodriverPkgVersion" > .geckodriver/pkgVersion - -echo "Creating bootstrap_cache archive" - -# archive cacheable directories -mkdir -p "$HOME/.kibana/bootstrap_cache" -tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ - x-pack/plugins/reporting/.chromium \ - .es \ - .chromedriver \ - .geckodriver; - -echo "Adding node_modules" -# Find all of the node_modules directories that aren't test fixtures, and aren't inside other node_modules directories, and append them to the tar -find . -type d -name node_modules -not -path '*__fixtures__*' -prune -print0 | xargs -0I % tar -rf "$HOME/.kibana/bootstrap_cache/$branch.tar" "%" - -echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" - -if [ "$branch" == "master" ]; then - echo "Creating bootstrap cache for 7.x"; - - git clone https://github.com/elastic/kibana.git --branch 7.x --depth 1 /tmp/kibana-7.x - (cd /tmp/kibana-7.x && ./.ci/packer_cache.sh); - rm -rf /tmp/kibana-7.x; -fi +./.ci/packer_cache_for_branch.sh master +./.ci/packer_cache_for_branch.sh 7.x diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh new file mode 100755 index 0000000000000..a9fbe781915b6 --- /dev/null +++ b/.ci/packer_cache_for_branch.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -e + +branch="$1" +checkoutDir="$(pwd)" + +if [[ "$branch" != "master" ]]; then + checkoutDir="/tmp/kibana-$branch" + git clone https://github.com/elastic/kibana.git --branch "$branch" --depth 1 "$checkoutDir" + cd "$checkoutDir" +fi + +source src/dev/ci_setup/setup.sh; + +# download es snapshots +node scripts/es snapshot --download-only; +node scripts/es snapshot --license=oss --download-only; + +# download reporting browsers +(cd "x-pack" && yarn gulp prepare); + +# cache the chromedriver archive +chromedriverDistVersion="$(node -e "console.log(require('chromedriver').version)")" +chromedriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.chromedriver)")" +if [ -z "$chromedriverDistVersion" ] || [ -z "$chromedriverPkgVersion" ]; then + echo "UNABLE TO DETERMINE CHROMEDRIVER VERSIONS" + exit 1 +fi +mkdir -p .chromedriver +curl "https://chromedriver.storage.googleapis.com/$chromedriverDistVersion/chromedriver_linux64.zip" > .chromedriver/chromedriver.zip +echo "$chromedriverPkgVersion" > .chromedriver/pkgVersion + +# cache the geckodriver archive +geckodriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.geckodriver)")" +if [ -z "$geckodriverPkgVersion" ]; then + echo "UNABLE TO DETERMINE geckodriver VERSIONS" + exit 1 +fi +mkdir -p ".geckodriver" +cp "node_modules/geckodriver/geckodriver.tar.gz" .geckodriver/geckodriver.tar.gz +echo "$geckodriverPkgVersion" > .geckodriver/pkgVersion + +echo "Creating bootstrap_cache archive" + +# archive cacheable directories +mkdir -p "$HOME/.kibana/bootstrap_cache" +tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ + x-pack/plugins/reporting/.chromium \ + .es \ + .chromedriver \ + .geckodriver; + +echo "Adding node_modules" +# Find all of the node_modules directories that aren't test fixtures, and aren't inside other node_modules directories, and append them to the tar +find . -type d -name node_modules -not -path '*__fixtures__*' -prune -print0 | xargs -0I % tar -rf "$HOME/.kibana/bootstrap_cache/$branch.tar" "%" + +echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" + +if [[ "$branch" != "master" ]]; then + rm --preserve-root -rf "$checkoutDir" +fi From 8b0efca3b20810085e02059d8b7a1c03df0386c5 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 16 Jun 2020 14:14:09 -0700 Subject: [PATCH 32/68] [kbn/pm] only count cached project (#69113) Co-authored-by: spalger Co-authored-by: Elastic Machine --- packages/kbn-pm/dist/index.js | 2 +- packages/kbn-pm/src/commands/bootstrap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 1600d66ec7558..f6d008c9bf9be 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -8846,13 +8846,13 @@ const BootstrapCommand = { if (valid) { _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].debug(`[${project.name}] cache up to date`); + cachedProjectCount += 1; } caches.set(project, { file, valid }); - cachedProjectCount += 1; } } diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 80ccc5daecc56..f8e50a8247856 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -74,10 +74,10 @@ export const BootstrapCommand: ICommand = { if (valid) { log.debug(`[${project.name}] cache up to date`); + cachedProjectCount += 1; } caches.set(project, { file, valid }); - cachedProjectCount += 1; } } From 8bc8837e347c7db923b0d442dda220ae412c670a Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 16 Jun 2020 14:24:35 -0700 Subject: [PATCH 33/68] [Reporting] Prepare export type definitions for Task Manager (#65213) --- x-pack/plugins/reporting/public/plugin.tsx | 2 - .../common/execute_job/decrypt_job_headers.ts | 4 +- .../get_conditional_headers.test.ts | 16 +- .../execute_job/get_conditional_headers.ts | 4 +- .../execute_job/get_custom_logo.test.ts | 6 +- .../common/execute_job/get_custom_logo.ts | 4 +- .../common/execute_job/get_full_urls.test.ts | 8 +- .../common/execute_job/get_full_urls.ts | 20 +- .../execute_job/omit_blacklisted_headers.ts | 4 +- .../server/export_types/csv/index.ts | 14 +- .../export_types/csv/server/create_job.ts | 14 +- .../csv/server/execute_job.test.ts | 266 +++++++++--------- .../export_types/csv/server/execute_job.ts | 21 +- .../server/export_types/csv/types.d.ts | 4 +- .../csv_from_savedobject/index.ts | 12 +- .../server/create_job/create_job.ts | 106 ------- .../server/create_job/index.ts | 94 ++++++- .../server/execute_job.ts | 24 +- .../csv_from_savedobject/types.d.ts | 4 +- .../server/export_types/png/index.ts | 14 +- .../png/server/create_job/index.ts | 6 +- .../png/server/execute_job/index.test.ts | 24 +- .../png/server/execute_job/index.ts | 18 +- .../server/export_types/png/types.d.ts | 4 +- .../export_types/printable_pdf/index.ts | 14 +- .../printable_pdf/server/create_job/index.ts | 8 +- .../server/execute_job/index.test.ts | 24 +- .../printable_pdf/server/execute_job/index.ts | 18 +- .../export_types/printable_pdf/types.d.ts | 4 +- .../reporting/server/lib/create_queue.ts | 6 +- .../server/lib/create_worker.test.ts | 12 +- .../reporting/server/lib/create_worker.ts | 2 +- .../reporting/server/lib/enqueue_job.ts | 6 +- .../server/lib/export_types_registry.ts | 34 +-- .../generate_from_savedobject_immediate.ts | 22 +- .../server/routes/lib/get_document_payload.ts | 10 +- .../reporting/server/routes/types.d.ts | 4 +- x-pack/plugins/reporting/server/types.ts | 36 +-- 38 files changed, 423 insertions(+), 470 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index fcaa295a45ecc..aad3d9b026c6e 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -155,8 +155,6 @@ export class ReportingPublicPlugin implements Plugin { ); } - // FIXME: only perform these actions for authenticated routes - // Depends on https://github.com/elastic/kibana/pull/39477 public start(core: CoreStart) { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts index e5124c80601d7..579b5196ad4d9 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts @@ -14,14 +14,14 @@ interface HasEncryptedHeaders { // TODO merge functionality with CSV execute job export const decryptJobHeaders = async < JobParamsType, - JobDocPayloadType extends HasEncryptedHeaders + ScheduledTaskParamsType extends HasEncryptedHeaders >({ encryptionKey, job, logger, }: { encryptionKey?: string; - job: JobDocPayloadType; + job: ScheduledTaskParamsType; logger: LevelLogger; }): Promise> => { try { diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts index 5d651ad5f8aea..030ced5dc4b80 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts @@ -8,8 +8,8 @@ import sinon from 'sinon'; import { ReportingConfig } from '../../../'; import { ReportingCore } from '../../../core'; import { createMockReportingCore } from '../../../test_helpers'; -import { JobDocPayload } from '../../../types'; -import { JobDocPayloadPDF } from '../../printable_pdf/types'; +import { ScheduledTaskParams } from '../../../types'; +import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; let mockConfig: ReportingConfig; @@ -37,7 +37,7 @@ describe('conditions', () => { }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, + job: {} as ScheduledTaskParams, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -64,14 +64,14 @@ test('uses basePath from job when creating saved object service', async () => { baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, + job: {} as ScheduledTaskParams, filteredHeaders: permittedHeaders, config: mockConfig, }); const jobBasePath = '/sbp/s/marketing'; await getCustomLogo({ reporting: mockReportingPlugin, - job: { basePath: jobBasePath } as JobDocPayloadPDF, + job: { basePath: jobBasePath } as ScheduledTaskParamsPDF, conditionalHeaders, config: mockConfig, }); @@ -94,14 +94,14 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, + job: {} as ScheduledTaskParams, filteredHeaders: permittedHeaders, config: mockConfig, }); await getCustomLogo({ reporting: mockReportingPlugin, - job: {} as JobDocPayloadPDF, + job: {} as ScheduledTaskParamsPDF, conditionalHeaders, config: mockConfig, }); @@ -139,7 +139,7 @@ describe('config formatting', () => { mockConfig = getMockConfig(mockConfigGet); const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, + job: {} as ScheduledTaskParams, filteredHeaders: {}, config: mockConfig, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts index 6854f678aa975..7a50eaac80d85 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts @@ -7,13 +7,13 @@ import { ReportingConfig } from '../../../'; import { ConditionalHeaders } from '../../../types'; -export const getConditionalHeaders = ({ +export const getConditionalHeaders = ({ config, job, filteredHeaders, }: { config: ReportingConfig; - job: JobDocPayloadType; + job: ScheduledTaskParamsType; filteredHeaders: Record; }) => { const { kbnConfig } = config; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts index bd6eb4644d87f..c364752c8dd0f 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts @@ -6,7 +6,7 @@ import { ReportingCore } from '../../../core'; import { createMockReportingCore } from '../../../test_helpers'; -import { JobDocPayloadPDF } from '../../printable_pdf/types'; +import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; const mockConfigGet = jest.fn().mockImplementation((key: string) => { @@ -37,7 +37,7 @@ test(`gets logo from uiSettings`, async () => { }); const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayloadPDF, + job: {} as ScheduledTaskParamsPDF, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -45,7 +45,7 @@ test(`gets logo from uiSettings`, async () => { const { logo } = await getCustomLogo({ reporting: mockReportingPlugin, config: mockConfig, - job: {} as JobDocPayloadPDF, + job: {} as ScheduledTaskParamsPDF, conditionalHeaders, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts index 85d1272fc22ce..36c02eb47565c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts @@ -7,7 +7,7 @@ import { ReportingConfig, ReportingCore } from '../../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; import { ConditionalHeaders } from '../../../types'; -import { JobDocPayloadPDF } from '../../printable_pdf/types'; // Logo is PDF only +import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, @@ -17,7 +17,7 @@ export const getCustomLogo = async ({ }: { reporting: ReportingCore; config: ReportingConfig; - job: JobDocPayloadPDF; + job: ScheduledTaskParamsPDF; conditionalHeaders: ConditionalHeaders; }) => { const serverBasePath: string = config.kbnConfig.get('server', 'basePath'); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts index cacea41477ea4..ad952c084d4f3 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts @@ -5,12 +5,12 @@ */ import { ReportingConfig } from '../../../'; -import { JobDocPayloadPNG } from '../../png/types'; -import { JobDocPayloadPDF } from '../../printable_pdf/types'; +import { ScheduledTaskParamsPNG } from '../../png/types'; +import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { - job: JobDocPayloadPNG & JobDocPayloadPDF; + job: ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; config: ReportingConfig; } @@ -35,7 +35,7 @@ beforeEach(() => { mockConfig = getMockConfig(mockConfigGet); }); -const getMockJob = (base: object) => base as JobDocPayloadPNG & JobDocPayloadPDF; +const getMockJob = (base: object) => base as ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; test(`fails if no URL is passed`, async () => { const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts index bcd7f122748cb..67bc8d16fa758 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts @@ -13,22 +13,26 @@ import { import { ReportingConfig } from '../../..'; import { getAbsoluteUrlFactory } from '../../../../common/get_absolute_url'; import { validateUrls } from '../../../../common/validate_urls'; -import { JobDocPayloadPNG } from '../../png/types'; -import { JobDocPayloadPDF } from '../../printable_pdf/types'; +import { ScheduledTaskParamsPNG } from '../../png/types'; +import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; -function isPngJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloadPNG { - return (job as JobDocPayloadPNG).relativeUrl !== undefined; +function isPngJob( + job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF +): job is ScheduledTaskParamsPNG { + return (job as ScheduledTaskParamsPNG).relativeUrl !== undefined; } -function isPdfJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloadPDF { - return (job as JobDocPayloadPDF).relativeUrls !== undefined; +function isPdfJob( + job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF +): job is ScheduledTaskParamsPDF { + return (job as ScheduledTaskParamsPDF).relativeUrls !== undefined; } -export function getFullUrls({ +export function getFullUrls({ config, job, }: { config: ReportingConfig; - job: JobDocPayloadPDF | JobDocPayloadPNG; + job: ScheduledTaskParamsPDF | ScheduledTaskParamsPNG; }) { const [basePath, protocol, hostname, port] = [ config.kbnConfig.get('server', 'basePath'), diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts index 5147881a980ea..db7137c30513b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts @@ -9,11 +9,11 @@ import { KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, } from '../../../../common/constants'; -export const omitBlacklistedHeaders = ({ +export const omitBlacklistedHeaders = ({ job, decryptedHeaders, }: { - job: JobDocPayloadType; + job: ScheduledTaskParamsType; decryptedHeaders: Record; }) => { const filteredHeaders: Record = omit( diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index 8642a6d5758a8..b5eacdfc62c8b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -15,21 +15,21 @@ import { import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadDiscoverCsv, JobParamsDiscoverCsv } from './types'; +import { scheduleTaskFnFactory } from './server/create_job'; +import { runTaskFnFactory } from './server/execute_job'; +import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsDiscoverCsv, ESQueueCreateJobFn, - JobDocPayloadDiscoverCsv, - ESQueueWorkerExecuteFn + ScheduledTaskParamsCSV, + ESQueueWorkerExecuteFn > => ({ ...metadata, jobType, jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, + scheduleTaskFnFactory, + runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, LICENSE_TYPE_BASIC, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts index acf7f0505a735..c4fa1cd8e4fa6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts @@ -4,24 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { ReportingCore } from '../../../'; import { cryptoFactory } from '../../../lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../types'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; import { JobParamsDiscoverCsv } from '../types'; -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); const setupDeps = reporting.getPluginSetupDeps(); - return async function createJob( - jobParams: JobParamsDiscoverCsv, - context: RequestHandlerContext, - request: KibanaRequest - ) { + return async function scheduleTask(jobParams, context, request) { const serializedEncryptedHeaders = await crypto.encrypt(request.headers); const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts index 4ce448e953bd1..d1297454971c7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts @@ -20,8 +20,8 @@ import { CSV_BOM_CHARS } from '../../../../common/constants'; import { LevelLogger } from '../../../lib'; import { setFieldFormats } from '../../../services'; import { createMockReportingCore } from '../../../test_helpers'; -import { JobDocPayloadDiscoverCsv } from '../types'; -import { executeJobFactory } from './execute_job'; +import { ScheduledTaskParamsCSV } from '../types'; +import { runTaskFnFactory } from './execute_job'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -30,7 +30,7 @@ const getRandomScrollId = () => { return puid.generate(); }; -const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadDiscoverCsv; +const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsCSV; describe('CSV Execute Job', function () { const encryptionKey = 'testEncryptionKey'; @@ -125,10 +125,10 @@ describe('CSV Execute Job', function () { describe('basic Elasticsearch call behavior', function () { it('should decrypt encrypted headers and pass to callAsCurrentUser', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - await executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + await runTask( 'job456', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -145,8 +145,8 @@ describe('CSV Execute Job', function () { testBody: true, }; - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const job = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const job = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { @@ -155,7 +155,7 @@ describe('CSV Execute Job', function () { }, }); - await executeJob('job777', job, cancellationToken); + await runTask('job777', job, cancellationToken); const searchCall = callAsCurrentUserStub.firstCall; expect(searchCall.args[0]).toBe('search'); @@ -172,10 +172,10 @@ describe('CSV Execute Job', function () { _scroll_id: scrollId, }); callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - await executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + await runTask( 'job456', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -190,10 +190,10 @@ describe('CSV Execute Job', function () { }); it('should not execute scroll if there are no hits from the search', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - await executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + await runTask( 'job456', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -224,10 +224,10 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - await executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + await runTask( 'job456', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -263,10 +263,10 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - await executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + await runTask( 'job456', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -295,16 +295,16 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: undefined, searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( + `[TypeError: Cannot read property 'indexOf' of undefined]` + ); const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); expect(lastCall.args[0]).toBe('clearScroll'); @@ -322,14 +322,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + const { csv_contains_formulas: csvContainsFormulas } = await runTask( 'job123', jobParams, cancellationToken @@ -347,14 +347,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + const { csv_contains_formulas: csvContainsFormulas } = await runTask( 'job123', jobParams, cancellationToken @@ -373,14 +373,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + const { csv_contains_formulas: csvContainsFormulas } = await runTask( 'job123', jobParams, cancellationToken @@ -399,15 +399,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + const { csv_contains_formulas: csvContainsFormulas } = await runTask( 'job123', jobParams, cancellationToken @@ -425,14 +425,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + const { csv_contains_formulas: csvContainsFormulas } = await runTask( 'job123', jobParams, cancellationToken @@ -452,14 +452,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toEqual(`${CSV_BOM_CHARS}one,two\none,bar\n`); }); @@ -473,14 +473,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toEqual('one,two\none,bar\n'); }); @@ -496,14 +496,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toEqual("one,two\n\"'=cmd|' /C calc'!A0\",bar\n"); }); @@ -517,14 +517,14 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toEqual('one,two\n"=cmd|\' /C calc\'!A0",bar\n'); }); @@ -533,15 +533,15 @@ describe('CSV Execute Job', function () { describe('Elasticsearch call errors', function () { it('should reject Promise if search call errors out', async function () { callAsCurrentUserStub.rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[Error]`); + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( + `[Error]` + ); }); it('should reject Promise if scroll call errors out', async function () { @@ -552,15 +552,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[Error]`); + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( + `[Error]` + ); }); }); @@ -573,15 +573,13 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` ); }); @@ -594,15 +592,13 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` ); }); @@ -622,15 +618,13 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` ); }); @@ -650,15 +644,13 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, }); - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( + await expect(runTask('job123', jobParams, cancellationToken)).rejects.toMatchInlineSnapshot( `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` ); }); @@ -686,10 +678,10 @@ describe('CSV Execute Job', function () { }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + runTask( 'job345', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -705,10 +697,10 @@ describe('CSV Execute Job', function () { }); it(`shouldn't call clearScroll if it never got a scrollId`, async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + runTask( 'job345', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -723,10 +715,10 @@ describe('CSV Execute Job', function () { }); it('should call clearScroll if it got a scrollId', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - executeJob( + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + runTask( 'job345', - getJobDocPayload({ + getScheduledTaskParams({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -745,54 +737,54 @@ describe('CSV Execute Job', function () { describe('csv content', function () { it('should write column headers to output, even if there are no results', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toBe(`one,two\n`); }); it('should use custom uiSettings csv:separator for header', async function () { mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toBe(`one;two\n`); }); it('should escape column headers if uiSettings csv:quoteValues is true', async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toBe(`"one and a half",two,"three-and-four","five & six"\n`); }); it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); expect(content).toBe(`one and a half,two,three-and-four,five & six\n`); }); it('should write column headers to output, when there are results', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], @@ -800,19 +792,19 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getJobDocPayload({ + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); const lines = content.split('\n'); const headerLine = lines[0]; expect(headerLine).toBe('one,two'); }); it('should use comma separated values of non-nested fields from _source', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -820,20 +812,20 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getJobDocPayload({ + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); const lines = content.split('\n'); const valuesLine = lines[1]; expect(valuesLine).toBe('foo,bar'); }); it('should concatenate the hits from multiple responses', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -847,13 +839,13 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getJobDocPayload({ + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); const lines = content.split('\n'); expect(lines[1]).toBe('foo,bar'); @@ -861,7 +853,7 @@ describe('CSV Execute Job', function () { }); it('should use field formatters to format fields', async function () { - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -869,7 +861,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getJobDocPayload({ + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -884,7 +876,7 @@ describe('CSV Execute Job', function () { }, }, }); - const { content } = await executeJob('job123', jobParams, cancellationToken); + const { content } = await runTask('job123', jobParams, cancellationToken); const lines = content.split('\n'); expect(lines[1]).toBe('FOO,bar'); @@ -903,14 +895,14 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, }); - ({ content, max_size_reached: maxSizeReached } = await executeJob( + ({ content, max_size_reached: maxSizeReached } = await runTask( 'job123', jobParams, cancellationToken @@ -933,14 +925,14 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, }); - ({ content, max_size_reached: maxSizeReached } = await executeJob( + ({ content, max_size_reached: maxSizeReached } = await runTask( 'job123', jobParams, cancellationToken @@ -970,15 +962,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - ({ content, max_size_reached: maxSizeReached } = await executeJob( + ({ content, max_size_reached: maxSizeReached } = await runTask( 'job123', jobParams, cancellationToken @@ -1010,15 +1002,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - ({ content, max_size_reached: maxSizeReached } = await executeJob( + ({ content, max_size_reached: maxSizeReached } = await runTask( 'job123', jobParams, cancellationToken @@ -1047,15 +1039,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - await executeJob('job123', jobParams, cancellationToken); + await runTask('job123', jobParams, cancellationToken); const searchCall = callAsCurrentUserStub.firstCall; expect(searchCall.args[0]).toBe('search'); @@ -1073,15 +1065,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - await executeJob('job123', jobParams, cancellationToken); + await runTask('job123', jobParams, cancellationToken); const searchCall = callAsCurrentUserStub.firstCall; expect(searchCall.args[0]).toBe('search'); @@ -1099,15 +1091,15 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingCore, mockLogger); - const jobParams = getJobDocPayload({ + const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, }); - await executeJob('job123', jobParams, cancellationToken); + await runTask('job123', jobParams, cancellationToken); const scrollCall = callAsCurrentUserStub.secondCall; expect(scrollCall.args[0]).toBe('scroll'); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts index 91a4db0469fb5..89fd014502f74 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts @@ -8,31 +8,26 @@ import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; import { - CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING, + CSV_SEPARATOR_SETTING, } from '../../../../../../../src/plugins/share/server'; -import { ReportingCore } from '../../..'; import { CSV_BOM_CHARS, CSV_JOB_TYPE } from '../../../../common/constants'; import { getFieldFormats } from '../../../../server/services'; -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory } from '../../../types'; -import { JobDocPayloadDiscoverCsv } from '../types'; +import { cryptoFactory } from '../../../lib'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../../types'; +import { ScheduledTaskParamsCSV } from '../types'; import { fieldFormatMapFactory } from './lib/field_format_map'; import { createGenerateCsv } from './lib/generate_csv'; -export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { +export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); const serverBasePath = config.kbnConfig.get('server', 'basePath'); - return async function executeJob( - jobId: string, - job: JobDocPayloadDiscoverCsv, - cancellationToken: any - ) { + return async function runTask(jobId, job, cancellationToken) { const elasticsearch = reporting.getElasticsearchService(); const jobLogger = logger.clone([jobId]); diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index c80cd5fd24fe5..ab3e114c7c995 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -5,7 +5,7 @@ */ import { CancellationToken } from '../../../common'; -import { JobParamPostPayload, JobDocPayload, ScrollConfig } from '../../types'; +import { JobParamPostPayload, ScheduledTaskParams, ScrollConfig } from '../../types'; export type RawValue = string | object | null | undefined; @@ -32,7 +32,7 @@ export interface JobParamsDiscoverCsv { post?: JobParamPostPayloadDiscoverCsv; } -export interface JobDocPayloadDiscoverCsv extends JobDocPayload { +export interface ScheduledTaskParamsCSV extends ScheduledTaskParams { basePath: string; searchRequest: any; fields: any; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 65802ee5bb7fb..961a046c846e4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -15,16 +15,16 @@ import { import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { createJobFactory, ImmediateCreateJobFn } from './server/create_job'; -import { executeJobFactory, ImmediateExecuteFn } from './server/execute_job'; +import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './server/create_job'; +import { ImmediateExecuteFn, runTaskFnFactory } from './server/execute_job'; import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { createJobFactory } from './server/create_job'; -export { executeJobFactory } from './server/execute_job'; +export { scheduleTaskFnFactory } from './server/create_job'; +export { runTaskFnFactory } from './server/execute_job'; export const getExportType = (): ExportTypeDefinition< JobParamsPanelCsv, @@ -35,8 +35,8 @@ export const getExportType = (): ExportTypeDefinition< ...metadata, jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, + scheduleTaskFnFactory, + runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, LICENSE_TYPE_BASIC, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts deleted file mode 100644 index c187da5104d3f..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { notFound, notImplemented } from 'boom'; -import { get } from 'lodash'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { ReportingCore } from '../../../..'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../../common/constants'; -import { cryptoFactory, LevelLogger } from '../../../../lib'; -import { CreateJobFactory, TimeRangeParams } from '../../../../types'; -import { - JobDocPayloadPanelCsv, - JobParamsPanelCsv, - SavedObject, - SavedObjectServiceError, - SavedSearchObjectAttributesJSON, - SearchPanel, - VisObjectAttributesJSON, -} from '../../types'; -import { createJobSearch } from './create_job_search'; - -export type ImmediateCreateJobFn = ( - jobParams: JobParamsType, - headers: KibanaRequest['headers'], - context: RequestHandlerContext, - req: KibanaRequest -) => Promise<{ - type: string | null; - title: string; - jobParams: JobParamsType; -}>; - -interface VisData { - title: string; - visType: string; - panel: SearchPanel; -} - -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); - - return async function createJob( - jobParams: JobParamsPanelCsv, - headers: KibanaRequest['headers'], - context: RequestHandlerContext, - req: KibanaRequest - ): Promise { - const { savedObjectType, savedObjectId } = jobParams; - const serializedEncryptedHeaders = await crypto.encrypt(headers); - - const { panel, title, visType }: VisData = await Promise.resolve() - .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) - .then(async (savedObject: SavedObject) => { - const { attributes, references } = savedObject; - const { - kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, - } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.body as { timerange: TimeRangeParams }; - - if (!kibanaSavedObjectMetaJSON) { - throw new Error('Could not parse saved object data!'); - } - - const kibanaSavedObjectMeta = { - ...kibanaSavedObjectMetaJSON, - searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON), - }; - - const { visState: visStateJSON } = attributes as VisObjectAttributesJSON; - if (visStateJSON) { - throw notImplemented('Visualization types are not yet implemented'); - } - - // saved search type - return await createJobSearch(timerange, attributes, references, kibanaSavedObjectMeta); - }) - .catch((err: Error) => { - const boomErr = (err as unknown) as { isBoom: boolean }; - if (boomErr.isBoom) { - throw err; - } - const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 }); - if (errPayload.statusCode === 404) { - throw notFound(errPayload.message); - } - if (err.stack) { - logger.error(err.stack); - } - throw new Error(`Unable to create a job from saved object data! Error: ${err}`); - }); - - return { - headers: serializedEncryptedHeaders, - jobParams: { ...jobParams, panel, visType }, - type: null, - title, - }; - }; -}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts index a3674d69ae6a5..dafac04017607 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts @@ -4,4 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createJobFactory, ImmediateCreateJobFn } from './create_job'; +import { notFound, notImplemented } from 'boom'; +import { get } from 'lodash'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../../common/constants'; +import { cryptoFactory } from '../../../../lib'; +import { ScheduleTaskFnFactory, TimeRangeParams } from '../../../../types'; +import { + JobParamsPanelCsv, + SavedObject, + SavedObjectServiceError, + SavedSearchObjectAttributesJSON, + SearchPanel, + VisObjectAttributesJSON, +} from '../../types'; +import { createJobSearch } from './create_job_search'; + +export type ImmediateCreateJobFn = ( + jobParams: JobParamsType, + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest +) => Promise<{ + type: string | null; + title: string; + jobParams: JobParamsType; +}>; + +interface VisData { + title: string; + visType: string; + panel: SearchPanel; +} + +export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting, parentLogger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); + + return async function scheduleTask(jobParams, headers, context, req) { + const { savedObjectType, savedObjectId } = jobParams; + const serializedEncryptedHeaders = await crypto.encrypt(headers); + + const { panel, title, visType }: VisData = await Promise.resolve() + .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) + .then(async (savedObject: SavedObject) => { + const { attributes, references } = savedObject; + const { + kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, + } = attributes as SavedSearchObjectAttributesJSON; + const { timerange } = req.body as { timerange: TimeRangeParams }; + + if (!kibanaSavedObjectMetaJSON) { + throw new Error('Could not parse saved object data!'); + } + + const kibanaSavedObjectMeta = { + ...kibanaSavedObjectMetaJSON, + searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON), + }; + + const { visState: visStateJSON } = attributes as VisObjectAttributesJSON; + if (visStateJSON) { + throw notImplemented('Visualization types are not yet implemented'); + } + + // saved search type + return await createJobSearch(timerange, attributes, references, kibanaSavedObjectMeta); + }) + .catch((err: Error) => { + const boomErr = (err as unknown) as { isBoom: boolean }; + if (boomErr.isBoom) { + throw err; + } + const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 }); + if (errPayload.statusCode === 404) { + throw notFound(errPayload.message); + } + if (err.stack) { + logger.error(err.stack); + } + throw new Error(`Unable to create a job from saved object data! Error: ${err}`); + }); + + return { + headers: serializedEncryptedHeaders, + jobParams: { ...jobParams, panel, visType }, + type: null, + title, + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts index d555100b6320d..26b7a24907f40 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts @@ -6,39 +6,33 @@ import { i18n } from '@kbn/i18n'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { ReportingCore } from '../../..'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../types'; +import { cryptoFactory } from '../../../lib'; +import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../../types'; import { CsvResultFromSearch } from '../../csv/types'; -import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; +import { FakeRequest, JobParamsPanelCsv, SearchPanel } from '../types'; import { createGenerateCsv } from './lib'; /* * ImmediateExecuteFn receives the job doc payload because the payload was - * generated in the CreateFn + * generated in the ScheduleFn */ export type ImmediateExecuteFn = ( jobId: null, - job: JobDocPayload, + job: ScheduledTaskParams, context: RequestHandlerContext, req: KibanaRequest -) => Promise; +) => Promise; -export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { +>> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); const generateCsv = createGenerateCsv(reporting, parentLogger); - return async function executeJob( - jobId: string | null, - job: JobDocPayloadPanelCsv, - context, - req - ): Promise { + return async function runTask(jobId: string | null, job, context, req) { // There will not be a jobID for "immediate" generation. // jobID is only for "queued" jobs // Use the jobID as a logging tag or "immediate" diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts index 36ae5b1dac05e..835b352953dfe 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamPostPayload, JobDocPayload, TimeRangeParams } from '../../types'; +import { JobParamPostPayload, ScheduledTaskParams, TimeRangeParams } from '../../types'; export interface FakeRequest { headers: Record; @@ -23,7 +23,7 @@ export interface JobParamsPanelCsv { visType?: string; } -export interface JobDocPayloadPanelCsv extends JobDocPayload { +export interface ScheduledTaskParamsPanelCsv extends ScheduledTaskParams { jobParams: JobParamsPanelCsv; } diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index a3b51e365e772..b708448b0f8b2 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -14,22 +14,22 @@ import { } from '../../../common/constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../..//types'; import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadPNG, JobParamsPNG } from './types'; +import { scheduleTaskFnFactory } from './server/create_job'; +import { runTaskFnFactory } from './server/execute_job'; +import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPNG, ESQueueCreateJobFn, - JobDocPayloadPNG, - ESQueueWorkerExecuteFn + ScheduledTaskParamsPNG, + ESQueueWorkerExecuteFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'PNG', - createJobFactory, - executeJobFactory, + scheduleTaskFnFactory, + runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, LICENSE_TYPE_STANDARD, diff --git a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts index 3f1556fb29782..f459b8f249c70 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts @@ -6,17 +6,17 @@ import { validateUrls } from '../../../../../common/validate_urls'; import { cryptoFactory } from '../../../../lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../types'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; import { JobParamsPNG } from '../../types'; -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob( + return async function scheduleTask( { objectType, title, relativeUrl, browserTimezone, layout }, context, req diff --git a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts index b92f53ff6563f..3d3f156aeef02 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts @@ -9,9 +9,9 @@ import { ReportingCore } from '../../../../'; import { CancellationToken } from '../../../../../common'; import { cryptoFactory, LevelLogger } from '../../../../lib'; import { createMockReportingCore } from '../../../../test_helpers'; -import { JobDocPayloadPNG } from '../../types'; +import { ScheduledTaskParamsPNG } from '../../types'; import { generatePngObservableFactory } from '../lib/generate_png'; -import { executeJobFactory } from './'; +import { runTaskFnFactory } from './'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPNG; +const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPNG; beforeEach(async () => { const kbnConfig = { @@ -81,11 +81,11 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; - await executeJob( + await runTask( 'pngJobId', - getJobDocPayload({ + getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders, @@ -125,15 +125,15 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of('foo')); - const { content_type: contentType } = await executeJob( + const { content_type: contentType } = await runTask( 'pngJobId', - getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('image/png'); @@ -144,11 +144,11 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ base64: testContent })); - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( + const { content } = await runTask( 'pngJobId', - getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts index ea4c4b1d106ae..c9ab890dc8a50 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts @@ -7,37 +7,35 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { ReportingCore } from '../../../..'; import { PNG_JOB_TYPE } from '../../../../../common/constants'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../..//types'; -import { LevelLogger } from '../../../../lib'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../..//types'; import { decryptJobHeaders, getConditionalHeaders, getFullUrls, omitBlacklistedHeaders, } from '../../../common/execute_job/'; -import { JobDocPayloadPNG } from '../../types'; +import { ScheduledTaskParamsPNG } from '../../types'; import { generatePngObservableFactory } from '../lib/generate_png'; -type QueuedPngExecutorFactory = ExecuteJobFactory>; +type QueuedPngExecutorFactory = RunTaskFnFactory>; -export const executeJobFactory: QueuedPngExecutorFactory = async function executeJobFactoryFn( - reporting: ReportingCore, - parentLogger: LevelLogger +export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( + reporting, + parentLogger ) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); - return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { + return async function runTask(jobId, job, cancellationToken) { const apmTrans = apm.startTransaction('reporting execute_job png', 'reporting'); const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = logger.clone([jobId]); - const process$: Rx.Observable = Rx.of(1).pipe( + const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 486a8e91a722f..7a25f4ed8fe73 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobDocPayload } from '../../../server/types'; +import { ScheduledTaskParams } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../common/layouts'; // Job params: structure of incoming user request data @@ -17,7 +17,7 @@ export interface JobParamsPNG { } // Job payload: structure of stored job data provided by create_job -export interface JobDocPayloadPNG extends JobDocPayload { +export interface ScheduledTaskParamsPNG extends ScheduledTaskParams { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index 39a0cbd5270a1..073bd38b538fb 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -14,22 +14,22 @@ import { } from '../../../common/constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadPDF, JobParamsPDF } from './types'; +import { scheduleTaskFnFactory } from './server/create_job'; +import { runTaskFnFactory } from './server/execute_job'; +import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPDF, ESQueueCreateJobFn, - JobDocPayloadPDF, - ESQueueWorkerExecuteFn + ScheduledTaskParamsPDF, + ESQueueWorkerExecuteFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'pdf', - createJobFactory, - executeJobFactory, + scheduleTaskFnFactory, + runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, LICENSE_TYPE_STANDARD, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts index 06a0902a56954..76c5718249720 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts @@ -6,18 +6,18 @@ import { validateUrls } from '../../../../../common/validate_urls'; import { cryptoFactory } from '../../../../lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../types'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; import { JobParamsPDF } from '../../types'; -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJobFn( - { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, + return async function scheduleTaskFn( + { title, relativeUrls, browserTimezone, layout, objectType }, context, req ) { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts index 2f4ca47cf739e..d4df8ef7072d3 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts @@ -11,9 +11,9 @@ import { ReportingCore } from '../../../../'; import { CancellationToken } from '../../../../../common'; import { cryptoFactory, LevelLogger } from '../../../../lib'; import { createMockReportingCore } from '../../../../test_helpers'; -import { JobDocPayloadPDF } from '../../types'; +import { ScheduledTaskParamsPDF } from '../../types'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { executeJobFactory } from './'; +import { runTaskFnFactory } from './'; let mockReporting: ReportingCore; @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPDF; +const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPDF; beforeEach(async () => { const kbnConfig = { @@ -79,11 +79,11 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; - await executeJob( + await runTask( 'pdfJobId', - getJobDocPayload({ + getScheduledTaskParams({ title: 'PDF Params Timezone Test', relativeUrl: '/app/kibana#/something', browserTimezone, @@ -98,15 +98,15 @@ test(`passes browserTimezone to generatePdf`, async () => { test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); - const executeJob = await executeJobFactory(mockReporting, logger); + const runTask = await runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); - const { content_type: contentType } = await executeJob( + const { content_type: contentType } = await runTask( 'pdfJobId', - getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('application/pdf'); @@ -117,11 +117,11 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( + const { content } = await runTask( 'pdfJobId', - getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts index a4d84b2f9f1e0..7f8f2f4f6906a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts @@ -7,10 +7,8 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { ReportingCore } from '../../../..'; import { PDF_JOB_TYPE } from '../../../../../common/constants'; -import { LevelLogger } from '../../../../lib'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../../types'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -18,21 +16,21 @@ import { getFullUrls, omitBlacklistedHeaders, } from '../../../common/execute_job'; -import { JobDocPayloadPDF } from '../../types'; +import { ScheduledTaskParamsPDF } from '../../types'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -type QueuedPdfExecutorFactory = ExecuteJobFactory>; +type QueuedPdfExecutorFactory = RunTaskFnFactory>; -export const executeJobFactory: QueuedPdfExecutorFactory = async function executeJobFactoryFn( - reporting: ReportingCore, - parentLogger: LevelLogger +export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( + reporting, + parentLogger ) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); - return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { + return async function runTask(jobId, job, cancellationToken) { const apmTrans = apm.startTransaction('reporting execute_job pdf', 'reporting'); const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; @@ -40,7 +38,7 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut const generatePdfObservable = await generatePdfObservableFactory(reporting); const jobLogger = logger.clone([jobId]); - const process$: Rx.Observable = Rx.of(1).pipe( + const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 087ef5a6ca82c..5399781a77753 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobDocPayload } from '../../../server/types'; +import { ScheduledTaskParams } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../common/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON @@ -17,7 +17,7 @@ export interface JobParamsPDF { } // Job payload: structure of stored job data provided by create_job -export interface JobDocPayloadPDF extends JobDocPayload { +export interface ScheduledTaskParamsPDF extends ScheduledTaskParams { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/lib/create_queue.ts b/x-pack/plugins/reporting/server/lib/create_queue.ts index d993a17c0b314..5d09af312a41b 100644 --- a/x-pack/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/plugins/reporting/server/lib/create_queue.ts @@ -5,7 +5,7 @@ */ import { ReportingCore } from '../core'; -import { JobDocOutput, JobSource } from '../types'; +import { JobSource, TaskRunResult } from '../types'; import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed import { createWorkerFactory } from './create_worker'; import { Job } from './enqueue_job'; @@ -31,11 +31,11 @@ export interface ESQueueInstance { ) => ESQueueWorker; } -// GenericWorkerFn is a generic for ImmediateExecuteFn | ESQueueWorkerExecuteFn, +// GenericWorkerFn is a generic for ImmediateExecuteFn | ESQueueWorkerExecuteFn, type GenericWorkerFn = ( jobSource: JobSource, ...workerRestArgs: any[] -) => void | Promise; +) => void | Promise; export async function createQueueFactory( reporting: ReportingCore, diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts index 8e1174e01aa7f..85188c07eeb20 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -26,7 +26,7 @@ const executeJobFactoryStub = sinon.stub(); const getMockLogger = sinon.stub(); const getMockExportTypesRegistry = ( - exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] + exportTypes: any[] = [{ runTaskFnFactory: executeJobFactoryStub }] ) => ({ getAll: () => exportTypes, @@ -75,11 +75,11 @@ Object { test('Creates a single Esqueue worker for Reporting, even if there are multiple export types', async () => { const exportTypesRegistry = getMockExportTypesRegistry([ - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, + { runTaskFnFactory: executeJobFactoryStub }, + { runTaskFnFactory: executeJobFactoryStub }, + { runTaskFnFactory: executeJobFactoryStub }, + { runTaskFnFactory: executeJobFactoryStub }, + { runTaskFnFactory: executeJobFactoryStub }, ]); mockReporting.getExportTypesRegistry = () => exportTypesRegistry; const createWorker = createWorkerFactory(mockReporting, getMockLogger()); diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index c9e865668bb30..837be1f44a093 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -27,7 +27,7 @@ export function createWorkerFactory(reporting: ReportingCore, log for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< ExportTypeDefinition> >) { - const jobExecutor = await exportType.executeJobFactory(reporting, logger); // FIXME: does not "need" to be async + const jobExecutor = exportType.runTaskFnFactory(reporting, logger); jobExecutors.set(exportType.jobType, jobExecutor); } diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 3837f593df5b2..625da90f3b4f2 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -52,7 +52,7 @@ export function enqueueJobFactory( context: RequestHandlerContext, request: KibanaRequest ): Promise { - type CreateJobFn = ESQueueCreateJobFn; + type ScheduleTaskFnType = ESQueueCreateJobFn; const username = user ? user.username : false; const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -61,8 +61,8 @@ export function enqueueJobFactory( throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } - const createJob = exportType.createJobFactory(reporting, logger) as CreateJobFn; - const payload = await createJob(jobParams, context, request); + const scheduleTask = exportType.scheduleTaskFnFactory(reporting, logger) as ScheduleTaskFnType; + const payload = await scheduleTask(jobParams, context, request); const options = { timeout: queueTimeout, diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 893a2635561ff..501989f21103e 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -5,15 +5,14 @@ */ import { isString } from 'lodash'; -import memoizeOne from 'memoize-one'; import { getExportType as getTypeCsv } from '../export_types/csv'; import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_from_savedobject'; import { getExportType as getTypePng } from '../export_types/png'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; import { ExportTypeDefinition } from '../types'; -type GetCallbackFn = ( - item: ExportTypeDefinition +type GetCallbackFn = ( + item: ExportTypeDefinition ) => boolean; // => ExportTypeDefinition @@ -22,8 +21,8 @@ export class ExportTypesRegistry { constructor() {} - register( - item: ExportTypeDefinition + register( + item: ExportTypeDefinition ): void { if (!isString(item.id)) { throw new Error(`'item' must have a String 'id' property `); @@ -33,8 +32,6 @@ export class ExportTypesRegistry { throw new Error(`'item' with id ${item.id} has already been registered`); } - // TODO: Unwrap the execute function from the item's executeJobFactory - // Move that work out of server/lib/create_worker to reduce dependence on ESQueue this._map.set(item.id, item); } @@ -46,24 +43,24 @@ export class ExportTypesRegistry { return this._map.size; } - getById( + getById( id: string - ): ExportTypeDefinition { + ): ExportTypeDefinition { if (!this._map.has(id)) { throw new Error(`Unknown id ${id}`); } return this._map.get(id) as ExportTypeDefinition< JobParamsType, - CreateJobFnType, + ScheduleTaskFnType, JobPayloadType, - ExecuteJobFnType + RunTaskFnType >; } - get( - findType: GetCallbackFn - ): ExportTypeDefinition { + get( + findType: GetCallbackFn + ): ExportTypeDefinition { let result; for (const value of this._map.values()) { if (!findType(value)) { @@ -71,9 +68,9 @@ export class ExportTypesRegistry { } const foundResult: ExportTypeDefinition< JobParamsType, - CreateJobFnType, + ScheduleTaskFnType, JobPayloadType, - ExecuteJobFnType + RunTaskFnType > = value; if (result) { @@ -91,7 +88,7 @@ export class ExportTypesRegistry { } } -function getExportTypesRegistryFn(): ExportTypesRegistry { +export function getExportTypesRegistry(): ExportTypesRegistry { const registry = new ExportTypesRegistry(); /* this replaces the previously async method of registering export types, @@ -108,6 +105,3 @@ function getExportTypesRegistryFn(): ExportTypesRegistry { }); return registry; } - -// FIXME: is this the best way to return a singleton? -export const getExportTypesRegistry = memoizeOne(getExportTypesRegistryFn); diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 1221f67855410..7d93a36c85bc8 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,12 +7,12 @@ import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { createJobFactory } from '../export_types/csv_from_savedobject/server/create_job'; -import { executeJobFactory } from '../export_types/csv_from_savedobject/server/execute_job'; +import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/server/create_job'; +import { runTaskFnFactory } from '../export_types/csv_from_savedobject/server/execute_job'; import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { JobDocPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; +import { ScheduledTaskParamsPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; -import { JobDocOutput } from '../types'; +import { TaskRunResult } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; import { HandlerErrorFunction } from './types'; @@ -36,8 +36,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: - * - re-use the createJob function to build up es query config - * - re-use the executeJob function to run the scan and scroll queries and capture the entire CSV in a result object. + * - re-use the scheduleTask function to build up es query config + * - re-use the runTask function to run the scan and scroll queries and capture the entire CSV in a result object. */ router.post( { @@ -60,11 +60,11 @@ export function registerGenerateCsvFromSavedObjectImmediate( userHandler(async (user, context, req, res) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); - const createJobFn = createJobFactory(reporting, logger); - const executeJobFn = await executeJobFactory(reporting, logger); // FIXME: does not "need" to be async + const scheduleTaskFn = scheduleTaskFnFactory(reporting, logger); + const runTaskFn = runTaskFnFactory(reporting, logger); try { - const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + const jobDocPayload: ScheduledTaskParamsPanelCsv = await scheduleTaskFn( jobParams, req.headers, context, @@ -74,13 +74,13 @@ export function registerGenerateCsvFromSavedObjectImmediate( content_type: jobOutputContentType, content: jobOutputContent, size: jobOutputSize, - }: JobDocOutput = await executeJobFn(null, jobDocPayload, context, req); + }: TaskRunResult = await runTaskFn(null, jobDocPayload, context, req); logger.info(`Job output size: ${jobOutputSize} bytes`); /* * ESQueue worker function defaults `content` to null, even if the - * executeJob returned undefined. + * runTask returned undefined. * * This converts null to undefined so the value can be sent to h.response() */ diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index e16f5278c8cc7..93f79bfd892b9 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -10,7 +10,7 @@ import * as _ from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { statuses } from '../../lib/esqueue/constants/statuses'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; -import { ExportTypeDefinition, JobDocOutput, JobSource } from '../../types'; +import { ExportTypeDefinition, JobSource, TaskRunResult } from '../../types'; type ExportTypeType = ExportTypeDefinition; @@ -18,7 +18,7 @@ interface ErrorFromPayload { message: string; } -// A camelCase version of JobDocOutput +// A camelCase version of TaskRunResult interface Payload { statusCode: number; content: string | Buffer | ErrorFromPayload; @@ -31,7 +31,7 @@ const DEFAULT_TITLE = 'report'; const getTitle = (exportType: ExportTypeType, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; -const getReportingHeaders = (output: JobDocOutput, exportType: ExportTypeType) => { +const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType) => { const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { @@ -55,7 +55,7 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist } } - function getCompleted(output: JobDocOutput, jobType: string, title: string): Payload { + function getCompleted(output: TaskRunResult, jobType: string, title: string): Payload { const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); @@ -73,7 +73,7 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist // @TODO: These should be semantic HTTP codes as 500/503's indicate // error then these are really operating properly. - function getFailure(output: JobDocOutput): Payload { + function getFailure(output: TaskRunResult): Payload { return { statusCode: 500, content: { diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index 5eceed0a7f2ab..607ce34ab9465 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -6,7 +6,7 @@ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; -import { JobDocPayload } from '../types'; +import { ScheduledTaskParams } from '../types'; export type HandlerFunction = ( user: AuthenticatedUser | null, @@ -23,7 +23,7 @@ export interface QueuedJobPayload { error?: boolean; source: { job: { - payload: JobDocPayload; + payload: ScheduledTaskParams; }; }; } diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 409a89899bee0..96eef81672610 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -58,7 +58,7 @@ export interface JobParamPostPayload { timerange: TimeRangeParams; } -export interface JobDocPayload { +export interface ScheduledTaskParams { headers?: string; // serialized encrypted headers jobParams: JobParamsType; title: string; @@ -70,13 +70,13 @@ export interface JobSource { _index: string; _source: { jobtype: string; - output: JobDocOutput; - payload: JobDocPayload; + output: TaskRunResult; + payload: ScheduledTaskParams; status: JobStatus; }; } -export interface JobDocOutput { +export interface TaskRunResult { content_type: string; content: string | null; size: number; @@ -173,43 +173,43 @@ export type ReportingSetup = object; * Internal Types */ +export type CaptureConfig = ReportingConfigType['capture']; +export type ScrollConfig = ReportingConfigType['csv']['scroll']; + export type ESQueueCreateJobFn = ( jobParams: JobParamsType, context: RequestHandlerContext, request: KibanaRequest ) => Promise; -export type ESQueueWorkerExecuteFn = ( +export type ESQueueWorkerExecuteFn = ( jobId: string, - job: JobDocPayloadType, - cancellationToken?: CancellationToken + job: ScheduledTaskParamsType, + cancellationToken: CancellationToken ) => Promise; -export type CaptureConfig = ReportingConfigType['capture']; -export type ScrollConfig = ReportingConfigType['csv']['scroll']; - -export type CreateJobFactory = ( +export type ScheduleTaskFnFactory = ( reporting: ReportingCore, logger: LevelLogger -) => CreateJobFnType; +) => ScheduleTaskFnType; -export type ExecuteJobFactory = ( +export type RunTaskFnFactory = ( reporting: ReportingCore, logger: LevelLogger -) => Promise; // FIXME: does not "need" to be async +) => RunTaskFnType; export interface ExportTypeDefinition< JobParamsType, - CreateJobFnType, + ScheduleTaskFnType, JobPayloadType, - ExecuteJobFnType + RunTaskFnType > { id: string; name: string; jobType: string; jobContentEncoding?: string; jobContentExtension: string; - createJobFactory: CreateJobFactory; - executeJobFactory: ExecuteJobFactory; + scheduleTaskFnFactory: ScheduleTaskFnFactory; + runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } From 5c87a27c4dfda3a214e4f76c941d218475f08255 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 16 Jun 2020 14:31:09 -0700 Subject: [PATCH 34/68] [APM] Fix service maps not loading when there are no APM ML jobs (#69240) * Closes #69238 by handling 404 thrown error from the query for APM ML jobs. * Improved coded readability * moved anomaly job fetch to new function getApmAnomalyDetectionJobs for improved readability and only handle a 404 status code response else throw --- .../lib/service_map/get_service_anomalies.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index ac76464e2f2e8..900141e9040ae 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -8,12 +8,33 @@ import { leftJoin } from '../../../common/utils/left_join'; import { Job as AnomalyDetectionJob } from '../../../../ml/server'; import { PromiseReturnType } from '../../../typings/common'; import { IEnvOptions } from './get_service_map'; +import { Setup } from '../helpers/setup_request'; import { APM_ML_JOB_GROUP_NAME, encodeForMlApi, } from '../../../common/ml_job_constants'; +async function getApmAnomalyDetectionJobs( + setup: Setup +): Promise { + const { ml } = setup; + + if (!ml) { + return []; + } + try { + const { jobs } = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP_NAME); + return jobs; + } catch (error) { + if (error.statusCode === 404) { + return []; + } + throw error; + } +} + type ApmMlJobCategory = NonNullable>; + export const getApmMlJobCategory = ( mlJob: AnomalyDetectionJob, serviceNames: string[] @@ -62,7 +83,10 @@ export async function getServiceAnomalies( return []; } - const { jobs: apmMlJobs } = await ml.anomalyDetectors.jobs('apm'); + const apmMlJobs = await getApmAnomalyDetectionJobs(options.setup); + if (apmMlJobs.length === 0) { + return []; + } const apmMlJobCategories = apmMlJobs .map((job) => getApmMlJobCategory(job, serviceNames)) .filter( From e03f2428b1914634774c34e683205b5ef3fb891b Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 16 Jun 2020 15:16:35 -0700 Subject: [PATCH 35/68] [BundleRefPlugin] resolve imports to files too (#69241) Co-authored-by: spalger --- examples/embeddable_examples/kibana.json | 2 +- .../src/worker/bundle_refs_plugin.ts | 133 +++++++++++------- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index b3ee0de096989..486c6322fad93 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -6,5 +6,5 @@ "ui": true, "requiredPlugins": ["embeddable"], "optionalPlugins": [], - "extraPublicDirs": ["public/todo", "public/hello_world"] + "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"] } diff --git a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts index 6defcaa787b7d..9c4d5ed7f8a98 100644 --- a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts +++ b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts @@ -40,51 +40,69 @@ interface RequestData { type Callback = (error?: any, result?: T) => void; type ModuleFactory = (data: RequestData, callback: Callback) => void; -/** - * Isolate the weired type juggling we have to do to add a hook to the webpack compiler - */ -function hookIntoCompiler( - compiler: webpack.Compiler, - handler: (context: string, request: string) => Promise -) { - compiler.hooks.compile.tap('BundleRefsPlugin', (compilationParams: any) => { - compilationParams.normalModuleFactory.hooks.factory.tap( - 'BundleRefsPlugin/normalModuleFactory/factory', - (wrappedFactory: ModuleFactory): ModuleFactory => (data, callback) => { - const context = data.context; - const dep = data.dependencies[0]; - - handler(context, dep.request).then( - (result) => { - if (!result) { - wrappedFactory(data, callback); - } else { - callback(undefined, result); - } - }, - (error) => callback(error) - ); - } - ); - }); -} - export class BundleRefsPlugin { - private resolvedRequestCache = new Map>(); + private readonly resolvedRefEntryCache = new Map>(); + private readonly resolvedRequestCache = new Map>(); + private readonly ignorePrefix = Path.resolve(this.bundle.contextDir) + Path.sep; - constructor(private readonly bundle: Bundle, public readonly bundleRefs: BundleRefs) {} + constructor(private readonly bundle: Bundle, private readonly bundleRefs: BundleRefs) {} - apply(compiler: webpack.Compiler) { - hookIntoCompiler(compiler, async (context, request) => { - const ref = await this.resolveRef(context, request); - if (ref) { - return new BundleRefModule(ref.exportId); + /** + * Called by webpack when the plugin is passed in the webpack config + */ + public apply(compiler: webpack.Compiler) { + // called whenever the compiler starts to compile, passed the params + // that will be used to create the compilation + compiler.hooks.compile.tap('BundleRefsPlugin', (compilationParams: any) => { + // clear caches because a new compilation is starting, meaning that files have + // changed and we should re-run resolutions + this.resolvedRefEntryCache.clear(); + this.resolvedRequestCache.clear(); + + // hook into the creation of NormalModule instances in webpack, if the import + // statement leading to the creation of the module is pointing to a bundleRef + // entry then create a BundleRefModule instead of a NormalModule. + compilationParams.normalModuleFactory.hooks.factory.tap( + 'BundleRefsPlugin/normalModuleFactory/factory', + (wrappedFactory: ModuleFactory): ModuleFactory => (data, callback) => { + const context = data.context; + const dep = data.dependencies[0]; + + this.maybeReplaceImport(context, dep.request).then( + (module) => { + if (!module) { + wrappedFactory(data, callback); + } else { + callback(undefined, module); + } + }, + (error) => callback(error) + ); + } + ); + }); + } + + private cachedResolveRefEntry(ref: BundleRef) { + const cached = this.resolvedRefEntryCache.get(ref); + + if (cached) { + return cached; + } + + const absoluteRequest = Path.resolve(ref.contextDir, ref.entry); + const promise = this.cachedResolveRequest(absoluteRequest).then((resolved) => { + if (!resolved) { + throw new Error(`Unable to resolve request [${ref.entry}] relative to [${ref.contextDir}]`); } + + return resolved; }); + this.resolvedRefEntryCache.set(ref, promise); + return promise; } - private cachedResolveRequest(context: string, request: string) { - const absoluteRequest = Path.resolve(context, request); + private cachedResolveRequest(absoluteRequest: string) { const cached = this.resolvedRequestCache.get(absoluteRequest); if (cached) { @@ -102,6 +120,7 @@ export class BundleRefsPlugin { return absoluteRequest; } + // look for an index file in directories if (stats?.isDirectory()) { for (const ext of RESOLVE_EXTENSIONS) { const indexPath = Path.resolve(absoluteRequest, `index${ext}`); @@ -112,6 +131,15 @@ export class BundleRefsPlugin { } } + // look for a file with one of the supported extensions + for (const ext of RESOLVE_EXTENSIONS) { + const filePath = `${absoluteRequest}${ext}`; + const fileStats = await safeStat(filePath); + if (fileStats?.isFile()) { + return filePath; + } + } + return; } @@ -121,7 +149,7 @@ export class BundleRefsPlugin { * then an error is thrown. If the request does not resolve to a bundleRef then * undefined is returned. Otherwise it returns the referenced bundleRef. */ - private async resolveRef(context: string, request: string) { + private async maybeReplaceImport(context: string, request: string) { // ignore imports that have loaders defined or are not relative seeming if (request.includes('!') || !request.startsWith('.')) { return; @@ -132,7 +160,12 @@ export class BundleRefsPlugin { return; } - const resolved = await this.cachedResolveRequest(context, request); + const absoluteRequest = Path.resolve(context, request); + if (absoluteRequest.startsWith(this.ignorePrefix)) { + return; + } + + const resolved = await this.cachedResolveRequest(absoluteRequest); if (!resolved) { return; } @@ -143,23 +176,17 @@ export class BundleRefsPlugin { return; } - let matchingRef: BundleRef | undefined; for (const ref of eligibleRefs) { - const resolvedEntry = await this.cachedResolveRequest(ref.contextDir, ref.entry); + const resolvedEntry = await this.cachedResolveRefEntry(ref); if (resolved === resolvedEntry) { - matchingRef = ref; - break; + return new BundleRefModule(ref.exportId); } } - if (!matchingRef) { - const bundleId = Array.from(new Set(eligibleRefs.map((r) => r.bundleId))).join(', '); - const publicDir = eligibleRefs.map((r) => r.entry).join(', '); - throw new Error( - `import [${request}] references a non-public export of the [${bundleId}] bundle and must point to one of the public directories: [${publicDir}]` - ); - } - - return matchingRef; + const bundleId = Array.from(new Set(eligibleRefs.map((r) => r.bundleId))).join(', '); + const publicDir = eligibleRefs.map((r) => r.entry).join(', '); + throw new Error( + `import [${request}] references a non-public export of the [${bundleId}] bundle and must point to one of the public directories: [${publicDir}]` + ); } } From ca9a1626a280f5e0ee783a3c92983d31b288a2b0 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 16 Jun 2020 17:03:59 -0700 Subject: [PATCH 36/68] [DOCS] Fixes license management links (#69347) --- docs/management/managing-beats.asciidoc | 2 +- docs/setup/docker.asciidoc | 2 +- docs/setup/install/deb.asciidoc | 2 +- docs/setup/install/rpm.asciidoc | 2 +- docs/setup/install/targz.asciidoc | 2 +- docs/setup/install/windows.asciidoc | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc index 26998a3b5b8f4..d5a9c52feae23 100644 --- a/docs/management/managing-beats.asciidoc +++ b/docs/management/managing-beats.asciidoc @@ -21,7 +21,7 @@ Don't have a license? You can start a 30-day trial. Open the menu, go to *Stack Management > Elasticsearch > License Management*. At the end of the trial period, you can purchase a subscription to keep using central management. For more information, see https://www.elastic.co/subscriptions and -{stack-ov}/license-management.html[License Management]. +<>. ==== {kib} makes it easy for you to use central management by walking you through the diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index ab7a85a2ff851..0dee112d15e86 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -13,7 +13,7 @@ https://github.com/elastic/dockerfiles/tree/{branch}/kibana[GitHub]. These images are free to use under the Elastic license. They contain open source and free commercial features and access to paid commercial features. -{stack-ov}/license-management.html[Start a 30-day trial] to try out all of the +<> to try out all of the paid commercial features. See the https://www.elastic.co/subscriptions[Subscriptions] page for information about Elastic license levels. diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index dfa1e3a37fd05..d24c1cf8ae9d1 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -10,7 +10,7 @@ Kibana on any Debian-based system such as Debian and Ubuntu. This package is free to use under the Elastic license. It contains open source and free commercial features and access to paid commercial features. -{stack-ov}/license-management.html[Start a 30-day trial] to try out all of the +<> to try out all of the paid commercial features. See the https://www.elastic.co/subscriptions[Subscriptions] page for information about Elastic license levels. diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index ccc38c2696158..5d4f47f300eac 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -15,7 +15,7 @@ such as SLES 11 and CentOS 5. Please see <> instead. This package is free to use under the Elastic license. It contains open source and free commercial features and access to paid commercial features. -{stack-ov}/license-management.html[Start a 30-day trial] to try out all of the +<> to try out all of the paid commercial features. See the https://www.elastic.co/subscriptions[Subscriptions] page for information about Elastic license levels. diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index c8bff5d58889d..14ee1b297ffc6 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -9,7 +9,7 @@ are the easiest formats to use when trying out Kibana. These packages are free to use under the Elastic license. They contain open source and free commercial features and access to paid commercial features. -{stack-ov}/license-management.html[Start a 30-day trial] to try out all of the +<> to try out all of the paid commercial features. See the https://www.elastic.co/subscriptions[Subscriptions] page for information about Elastic license levels. diff --git a/docs/setup/install/windows.asciidoc b/docs/setup/install/windows.asciidoc index 24bf74f607fef..0d467f2fa7dd9 100644 --- a/docs/setup/install/windows.asciidoc +++ b/docs/setup/install/windows.asciidoc @@ -8,7 +8,7 @@ Kibana can be installed on Windows using the `.zip` package. This package is free to use under the Elastic license. It contains open source and free commercial features and access to paid commercial features. -{stack-ov}/license-management.html[Start a 30-day trial] to try out all of the +<> to try out all of the paid commercial features. See the https://www.elastic.co/subscriptions[Subscriptions] page for information about Elastic license levels. From a34a3a7e090b371e625a64e2d533d3c4dc8f38fc Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 16 Jun 2020 21:48:28 -0400 Subject: [PATCH 37/68] [Component template] Details flyout (#68732) --- .../component_template_details.test.ts | 230 ++++++++++++++++++ .../component_template_list.test.ts | 5 +- .../component_template_details.helpers.ts | 86 +++++++ .../client_integration/helpers/constants.ts | 7 + .../helpers/http_requests.ts | 23 +- .../client_integration/helpers/index.ts | 2 + .../helpers/setup_environment.tsx | 6 +- .../component_template_details.tsx | 150 ++++++++++++ .../component_template_details/index.ts | 7 + .../manage_button.tsx | 105 ++++++++ .../tab_summary.tsx | 106 ++++++++ .../component_template_details/tabs.tsx | 70 ++++++ .../component_template_list.tsx | 50 +++- .../component_template_list_container.tsx | 16 +- .../component_template_list/table.tsx | 26 +- .../component_templates_context.tsx | 6 +- .../component_templates/constants.ts | 1 + .../components/component_templates/index.ts | 2 +- .../components/component_templates/lib/api.ts | 12 +- .../component_templates/shared_imports.ts | 8 + .../components/component_templates/types.ts | 17 -- .../shared/components/details_panel/index.ts | 9 + .../components/details_panel/tab_aliases.tsx | 39 +++ .../components/details_panel/tab_mappings.tsx | 38 +++ .../components/details_panel/tab_settings.tsx | 38 +++ .../components/shared/components/index.ts | 7 + .../application/components/shared/index.ts | 7 + .../public/application/index.tsx | 3 +- .../public/application/sections/home/home.tsx | 9 +- 29 files changed, 1044 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/types.ts create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/shared/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts new file mode 100644 index 0000000000000..7c17dde119c42 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, pageHelpers } from './helpers'; +import { ComponentTemplateDetailsTestBed } from './helpers/component_template_details.helpers'; +import { ComponentTemplateDeserialized } from '../../shared_imports'; + +const { setup } = pageHelpers.componentTemplateDetails; + +jest.mock('ui/i18n', () => { + const I18nContext = ({ children }: any) => children; + return { I18nContext }; +}); + +const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = { + name: 'comp-1', + template: { + mappings: { properties: { ip_address: { type: 'ip' } } }, + aliases: { mydata: {} }, + settings: { number_of_shards: 1 }, + }, + version: 1, + _meta: { description: 'component template test' }, + _kbnMeta: { usedBy: ['template_1'] }, +}; + +const COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS: ComponentTemplateDeserialized = { + name: 'comp-base', + template: {}, + _kbnMeta: { usedBy: [] }, +}; + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: ComponentTemplateDetailsTestBed; + + afterAll(() => { + server.restore(); + }); + + describe('With component template details', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('renders the details flyout', () => { + const { exists, find, actions, component } = testBed; + + // Verify flyout exists with correct title + expect(exists('componentTemplateDetails')).toBe(true); + expect(find('componentTemplateDetails.title').text()).toBe(COMPONENT_TEMPLATE.name); + + // Verify footer does not display since "actions" prop was not provided + expect(exists('componentTemplateDetails.footer')).toBe(false); + + // Verify tabs exist + expect(exists('settingsTab')).toBe(true); + expect(exists('mappingsTab')).toBe(true); + expect(exists('aliasesTab')).toBe(true); + // Summary tab should be active by default + expect(find('summaryTab').props()['aria-selected']).toBe(true); + + // [Summary tab] Verify description list items + expect(exists('summaryTabContent.usedByTitle')).toBe(true); + expect(exists('summaryTabContent.versionTitle')).toBe(true); + expect(exists('summaryTabContent.metaTitle')).toBe(true); + + // [Settings tab] Navigate to tab and verify content + act(() => { + actions.clickSettingsTab(); + }); + + component.update(); + + expect(exists('settingsTabContent')).toBe(true); + + // [Mappings tab] Navigate to tab and verify content + act(() => { + actions.clickMappingsTab(); + }); + + component.update(); + expect(exists('mappingsTabContent')).toBe(true); + + // [Aliases tab] Navigate to tab and verify content + act(() => { + actions.clickAliasesTab(); + }); + + component.update(); + expect(exists('aliasesTabContent')).toBe(true); + }); + }); + + describe('With only required component template fields', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse( + COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS + ); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('renders the details flyout', () => { + const { exists, actions, component } = testBed; + + // [Summary tab] Verify optional description list items do not display + expect(exists('summaryTabContent.usedByTitle')).toBe(false); + expect(exists('summaryTabContent.versionTitle')).toBe(false); + expect(exists('summaryTabContent.metaTitle')).toBe(false); + // Verify callout renders indicating the component template is not in use + expect(exists('notInUseCallout')).toBe(true); + + // [Settings tab] Navigate to tab and verify info callout + act(() => { + actions.clickSettingsTab(); + }); + + component.update(); + + expect(exists('noSettingsCallout')).toBe(true); + + // [Mappings tab] Navigate to tab and verify info callout + act(() => { + actions.clickMappingsTab(); + }); + + component.update(); + expect(exists('noMappingsCallout')).toBe(true); + + // [Aliases tab] Navigate to tab and verify info callout + act(() => { + actions.clickAliasesTab(); + }); + + component.update(); + expect(exists('noAliasesCallout')).toBe(true); + }); + }); + + describe('With actions', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + actions: [ + { + name: 'Test', + icon: 'info', + closePopoverOnClick: true, + handleActionClick: () => {}, + }, + ], + }); + }); + + testBed.component.update(); + }); + + test('should render a footer with context menu', () => { + const { exists, actions, component, find } = testBed; + + // Verify footer exists + expect(exists('componentTemplateDetails.footer')).toBe(true); + expect(exists('manageComponentTemplateButton')).toBe(true); + + // Click manage button and verify actions + act(() => { + actions.clickManageButton(); + }); + + component.update(); + + expect(exists('manageComponentTemplateContextMenu')).toBe(true); + expect(find('manageComponentTemplateContextMenu.action').length).toEqual(1); + }); + }); + + describe('Error handling', () => { + const error = { + status: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(undefined, { body: error }); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('should render an error message if error fetching pipelines', async () => { + const { exists, find } = testBed; + + expect(exists('sectionError')).toBe(true); + expect(find('sectionError').text()).toContain('Error loading component template'); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index 830cc0ee6a980..86eb88017b77f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -6,10 +6,11 @@ import { act } from 'react-dom/test-utils'; +import { ComponentTemplateListItem } from '../../shared_imports'; + import { setupEnvironment, pageHelpers } from './helpers'; import { ComponentTemplateListTestBed } from './helpers/component_template_list.helpers'; -import { API_BASE_PATH } from '../../../../../../common/constants'; -import { ComponentTemplateListItem } from '../../types'; +import { API_BASE_PATH } from './helpers/constants'; const { setup } = pageHelpers.componentTemplateList; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts new file mode 100644 index 0000000000000..25c2d654fd900 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerTestBed, TestBed } from '../../../../../../../../../test_utils'; +import { WithAppDependencies } from './setup_environment'; +import { ComponentTemplateDetailsFlyout } from '../../../component_template_details'; + +export type ComponentTemplateDetailsTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { find } = testBed; + + /** + * User Actions + */ + const clickSettingsTab = () => { + find('settingsTab').simulate('click'); + }; + + const clickMappingsTab = () => { + find('mappingsTab').simulate('click'); + }; + + const clickAliasesTab = () => { + find('aliasesTab').simulate('click'); + }; + + const clickManageButton = () => { + find('manageComponentTemplateButton').simulate('click'); + }; + + return { + clickSettingsTab, + clickAliasesTab, + clickMappingsTab, + clickManageButton, + }; +}; + +export const setup = (props: any): ComponentTemplateDetailsTestBed => { + const setupTestBed = registerTestBed( + WithAppDependencies(ComponentTemplateDetailsFlyout), + { + memoryRouter: { + wrapComponent: false, + }, + defaultProps: props, + } + ); + + const testBed = setupTestBed() as ComponentTemplateDetailsTestBed; + + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +export type ComponentTemplateDetailsTestSubjects = + | 'componentTemplateDetails' + | 'componentTemplateDetails.title' + | 'componentTemplateDetails.footer' + | 'summaryTab' + | 'mappingsTab' + | 'settingsTab' + | 'aliasesTab' + | 'sectionError' + | 'summaryTabContent' + | 'summaryTabContent.usedByTitle' + | 'summaryTabContent.versionTitle' + | 'summaryTabContent.metaTitle' + | 'notInUseCallout' + | 'aliasesTabContent' + | 'noAliasesCallout' + | 'mappingsTabContent' + | 'noMappingsCallout' + | 'settingsTabContent' + | 'noSettingsCallout' + | 'manageComponentTemplateButton' + | 'manageComponentTemplateContextMenu' + | 'manageComponentTemplateContextMenu.action'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts new file mode 100644 index 0000000000000..00b07fadd0c08 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_BASE_PATH = '/api/index_management'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index 8473041ee0af3..b7b674292dd98 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -5,11 +5,15 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { API_BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { API_BASE_PATH } from './constants'; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setLoadComponentTemplatesResponse = (response?: any[], error?: any) => { + const setLoadComponentTemplatesResponse = ( + response?: ComponentTemplateListItem[], + error?: any + ) => { const status = error ? error.status || 400 : 200; const body = error ? error.body : response; @@ -20,6 +24,20 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadComponentTemplateResponse = ( + response?: ComponentTemplateDeserialized, + error?: any + ) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/component_templates/:name`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setDeleteComponentTemplateResponse = (response?: object) => { server.respondWith('DELETE', `${API_BASE_PATH}/component_templates/:name`, [ 200, @@ -31,6 +49,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { return { setLoadComponentTemplatesResponse, setDeleteComponentTemplateResponse, + setLoadComponentTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts index c1d75b3c2dd9b..93eb65aac0761 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts @@ -5,6 +5,7 @@ */ import { setup as componentTemplatesListSetup } from './component_template_list.helpers'; +import { setup as componentTemplateDetailsSetup } from './component_template_details.helpers'; export { nextTick, getRandomString, findTestSubject } from '../../../../../../../../../test_utils'; @@ -12,4 +13,5 @@ export { setupEnvironment } from './setup_environment'; export const pageHelpers = { componentTemplateList: { setup: componentTemplatesListSetup }, + componentTemplateDetails: { setup: componentTemplateDetailsSetup }, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index c0aeb70166b5b..a2194bbfa0186 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -9,21 +9,21 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { HttpSetup } from 'kibana/public'; -import { BASE_PATH, API_BASE_PATH } from '../../../../../../../common/constants'; import { notificationServiceMock, docLinksServiceMock, } from '../../../../../../../../../../src/core/public/mocks'; -import { init as initHttpRequests } from './http_requests'; import { ComponentTemplatesProvider } from '../../../component_templates_context'; +import { init as initHttpRequests } from './http_requests'; +import { API_BASE_PATH } from './constants'; + const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); const appDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, apiBasePath: API_BASE_PATH, - appBasePath: BASE_PATH, trackMetric: () => {}, docLinks: docLinksServiceMock.createStartContract(), toasts: notificationServiceMock.createSetupContract().toasts, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx new file mode 100644 index 0000000000000..a8007c6363584 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; + +import { SectionLoading, TabSettings, TabAliases, TabMappings } from '../shared_imports'; +import { useComponentTemplatesContext } from '../component_templates_context'; +import { TabSummary } from './tab_summary'; +import { ComponentTemplateTabs, TabType } from './tabs'; +import { ManageButton, ManageAction } from './manage_button'; + +interface Props { + componentTemplateName: string; + onClose: () => void; + showFooter?: boolean; + actions?: ManageAction[]; +} + +export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ + componentTemplateName, + onClose, + actions, +}) => { + const { api } = useComponentTemplatesContext(); + + const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( + componentTemplateName + ); + + const [activeTab, setActiveTab] = useState('summary'); + + let content: React.ReactNode | undefined; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + color="danger" + iconType="alert" + data-test-subj="sectionError" + > +

{error.message}

+ + ); + } else if (componentTemplateDetails) { + const { + template: { settings, mappings, aliases }, + } = componentTemplateDetails; + + const tabToComponentMap: Record = { + summary: , + settings: , + mappings: , + aliases: , + }; + + const tabContent = tabToComponentMap[activeTab]; + + content = ( + <> + + + + + {tabContent} + + ); + } + + return ( + + + +

+ {componentTemplateName} +

+
+
+ + {content} + + {actions && ( + + + {/* "Close" link */} + + + + + + + {/* "Manage" context menu */} + {componentTemplateDetails && ( + + + + )} + + + )} +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts new file mode 100644 index 0000000000000..11aac200a2f14 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ComponentTemplateDetailsFlyout } from './component_template_details'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx new file mode 100644 index 0000000000000..c3a4f9b4dce35 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPopover, + EuiButton, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { ComponentTemplateDeserialized } from '../shared_imports'; + +export interface ManageAction { + name: string; + icon: string; + handleActionClick: () => void; + getIsDisabled?: (data: ComponentTemplateDeserialized) => boolean; + closePopoverOnClick?: boolean; +} + +interface Props { + actions: ManageAction[]; + componentTemplateDetails: ComponentTemplateDeserialized; +} + +export const ManageButton: React.FunctionComponent = ({ + actions, + componentTemplateDetails, +}) => { + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + const items: EuiContextMenuPanelItemDescriptor[] = actions.map( + ({ name, icon, getIsDisabled, closePopoverOnClick, handleActionClick }) => { + const isDisabled = getIsDisabled ? getIsDisabled(componentTemplateDetails) : false; + + return { + name, + icon, + disabled: isDisabled, + toolTipContent: isDisabled ? ( + + ) : null, + onClick: () => { + handleActionClick(); + + if (closePopoverOnClick) { + setIsPopOverOpen(false); + } + }, + 'data-test-subj': 'action', + }; + } + ); + + return ( + setIsPopOverOpen((prevBoolean) => !prevBoolean)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx new file mode 100644 index 0000000000000..401186f6c962e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiCodeBlock, + EuiTitle, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { ComponentTemplateDeserialized } from '../shared_imports'; + +interface Props { + componentTemplateDetails: ComponentTemplateDeserialized; +} + +export const TabSummary: React.FunctionComponent = ({ componentTemplateDetails }) => { + const { version, _meta, _kbnMeta } = componentTemplateDetails; + + const { usedBy } = _kbnMeta; + const templateIsInUse = usedBy.length > 0; + + return ( + <> + {/* Callout when component template is not in use */} + {!templateIsInUse && ( + <> + + } + iconType="pin" + data-test-subj="notInUseCallout" + size="s" + /> + + + )} + + {/* Summary description list */} + + {/* Used by templates */} + {templateIsInUse && ( + <> + + + + +
    + {usedBy.map((templateName: string) => ( +
  • + + {templateName} + +
  • + ))} +
+
+ + )} + + {/* Version (optional) */} + {version && ( + <> + + + + {version} + + )} + + {/* Metadata (optional) */} + {_meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} +
+ + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx new file mode 100644 index 0000000000000..89370e3f6f1b8 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTab, EuiTabs } from '@elastic/eui'; + +export type TabType = 'summary' | 'mappings' | 'aliases' | 'settings'; + +interface Props { + setActiveTab: (id: TabType) => void; + activeTab: TabType; +} + +interface Tab { + id: TabType; + name: string; +} + +const TABS: Tab[] = [ + { + id: 'summary', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: 'settings', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: 'mappings', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: 'aliases', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +export const ComponentTemplateTabs: React.FunctionComponent = ({ + setActiveTab, + activeTab, +}) => { + return ( + + {TABS.map((tab) => ( + { + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj={`${tab.id}Tab`} + > + {tab.name} + + ))} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 41fa608ef538b..05a5ed462d8f7 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -5,24 +5,39 @@ */ import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ScopedHistory } from 'kibana/public'; -import { SectionLoading } from '../shared_imports'; -import { useComponentTemplatesContext } from '../component_templates_context'; +import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; - +import { useComponentTemplatesContext } from '../component_templates_context'; +import { ComponentTemplateDetailsFlyout } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; import { LoadError } from './error'; import { ComponentTemplatesDeleteModal } from './delete_modal'; -export const ComponentTemplateList: React.FunctionComponent = () => { +interface Props { + componentTemplateName?: string; + history: RouteComponentProps['history']; +} + +export const ComponentTemplateList: React.FunctionComponent = ({ + componentTemplateName, + history, +}) => { const { api, trackMetric } = useComponentTemplatesContext(); const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates(); const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); + const goToList = () => { + return history.push('component_templates'); + }; + // Track component loaded useEffect(() => { trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); @@ -49,6 +64,7 @@ export const ComponentTemplateList: React.FunctionComponent = () => { componentTemplates={data} onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} + history={history as ScopedHistory} /> ); } else if (error) { @@ -58,18 +74,44 @@ export const ComponentTemplateList: React.FunctionComponent = () => { return (
{content} + + {/* delete modal */} {componentTemplatesToDelete?.length > 0 ? ( { if (deleteResponse?.hasDeletedComponentTemplates) { // refetch the component templates sendRequest(); + // go back to list view (if deleted from details flyout) + goToList(); } setComponentTemplatesToDelete([]); }} componentTemplatesToDelete={componentTemplatesToDelete} /> ) : null} + + {/* details flyout */} + {componentTemplateName && ( + + details._kbnMeta.usedBy.length > 0, + closePopoverOnClick: true, + handleActionClick: () => { + setComponentTemplatesToDelete([componentTemplateName]); + }, + }, + ]} + /> + )}
); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx index af8ab1b94c790..d0636da8cf93a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx @@ -5,16 +5,28 @@ */ import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { ComponentTemplatesAuthProvider } from './auth_provider'; import { ComponentTemplatesWithPrivileges } from './with_privileges'; import { ComponentTemplateList } from './component_template_list'; -export const ComponentTemplateListContainer: React.FunctionComponent = () => { +interface MatchParams { + componentTemplateName?: string; +} + +export const ComponentTemplateListContainer: React.FunctionComponent> = ({ + match: { + params: { componentTemplateName }, + }, + history, +}) => { return ( - + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index 2d9557e64e6e7..b67a249ae6976 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -12,21 +12,30 @@ import { EuiInMemoryTableProps, EuiTextColor, EuiIcon, + EuiLink, } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; -import { ComponentTemplateListItem } from '../types'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ComponentTemplateListItem } from '../shared_imports'; +import { UIM_COMPONENT_TEMPLATE_DETAILS } from '../constants'; +import { useComponentTemplatesContext } from '../component_templates_context'; export interface Props { componentTemplates: ComponentTemplateListItem[]; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; + history: ScopedHistory; } export const ComponentTable: FunctionComponent = ({ componentTemplates, onReloadClick, onDeleteClick, + history, }) => { + const { trackMetric } = useComponentTemplatesContext(); + const [selection, setSelection] = useState([]); const tableProps: EuiInMemoryTableProps = { @@ -120,6 +129,21 @@ export const ComponentTable: FunctionComponent = ({ defaultMessage: 'Name', }), sortable: true, + render: (name: string) => ( + /* eslint-disable-next-line @elastic/eui/href-or-on-click */ + trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) + )} + data-test-subj="templateDetailsLink" + > + {name} + + ), }, { field: 'usedBy', diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index 55f20ce21d417..e8116409def4b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -14,7 +14,6 @@ const ComponentTemplatesContext = createContext(undefined); interface Props { httpClient: HttpSetup; apiBasePath: string; - appBasePath: string; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; docLinks: DocLinksSetup; toasts: NotificationsSetup['toasts']; @@ -27,7 +26,6 @@ interface Context { documentation: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; toasts: NotificationsSetup['toasts']; - appBasePath: string; } export const ComponentTemplatesProvider = ({ @@ -37,7 +35,7 @@ export const ComponentTemplatesProvider = ({ value: Props; children: React.ReactNode; }) => { - const { httpClient, apiBasePath, trackMetric, docLinks, toasts, appBasePath } = value; + const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value; const useRequest = getUseRequest(httpClient); const sendRequest = getSendRequest(httpClient); @@ -47,7 +45,7 @@ export const ComponentTemplatesProvider = ({ return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index 501acde07fc00..e9acfa8dcc56d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -8,6 +8,7 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; +export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index e0219ec71787f..72e79a57ae413 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -8,4 +8,4 @@ export { ComponentTemplatesProvider } from './component_templates_context'; export { ComponentTemplateList } from './component_template_list'; -export * from './types'; +export { ComponentTemplateDetailsFlyout } from './component_template_details'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 351e83c6c0cb5..4a8cf965adfb9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem } from '../types'; -import { UseRequestHook, SendRequestHook } from './request'; +import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; +import { UseRequestHook, SendRequestHook } from './request'; export const getApi = ( useRequest: UseRequestHook, @@ -37,8 +37,16 @@ export const getApi = ( return result; } + function useLoadComponentTemplate(name: string) { + return useRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`, + method: 'get', + }); + } + return { useLoadComponentTemplates, deleteComponentTemplates, + useLoadComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index 049204f03c0c1..4e56f4a8c9818 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -19,3 +19,11 @@ export { useAuthorizationContext, NotAuthorizedSection, } from '../../../../../../../src/plugins/es_ui_shared/public'; + +export { TabMappings, TabSettings, TabAliases } from '../shared'; + +export { + ComponentTemplateSerialized, + ComponentTemplateDeserialized, + ComponentTemplateListItem, +} from '../../../../common'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/types.ts b/x-pack/plugins/index_management/public/application/components/component_templates/types.ts deleted file mode 100644 index 0aab3b6b0a94a..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// Ideally, we shouldn't depend on anything in index management that is -// outside of the components_templates directory -// We could consider creating shared types or duplicating the types here if -// the component_templates app were to move outside of index management -import { - ComponentTemplateSerialized, - ComponentTemplateDeserialized, - ComponentTemplateListItem, -} from '../../../../common'; - -export { ComponentTemplateSerialized, ComponentTemplateDeserialized, ComponentTemplateListItem }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts new file mode 100644 index 0000000000000..af5cd500a286a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TabAliases } from './tab_aliases'; +export { TabMappings } from './tab_mappings'; +export { TabSettings } from './tab_settings'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx new file mode 100644 index 0000000000000..bcf59fd13bad6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; + +import { Aliases } from '../../../../../../common'; + +interface Props { + aliases: Aliases | undefined; +} + +export const TabAliases: React.FunctionComponent = ({ aliases }) => { + if (aliases && Object.keys(aliases).length) { + return ( +
+ {JSON.stringify(aliases, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noAliasesCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx new file mode 100644 index 0000000000000..3c3d88345e5a1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; +import { Mappings } from '../../../../../../common'; + +interface Props { + mappings: Mappings | undefined; +} + +export const TabMappings: React.FunctionComponent = ({ mappings }) => { + if (mappings && Object.keys(mappings).length) { + return ( +
+ {JSON.stringify(mappings, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noMappingsCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx new file mode 100644 index 0000000000000..6d5cf09588a1c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; +import { IndexSettings } from '../../../../../../common'; + +interface Props { + settings: IndexSettings | undefined; +} + +export const TabSettings: React.FunctionComponent = ({ settings }) => { + if (settings && Object.keys(settings).length) { + return ( +
+ {JSON.stringify(settings, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noSettingsCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts new file mode 100644 index 0000000000000..90d66bd1a5da4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TabAliases, TabMappings, TabSettings } from './details_panel'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts new file mode 100644 index 0000000000000..e015ef72e244f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TabAliases, TabMappings, TabSettings } from './components'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index 5d1096c9ee24e..ff54b4b1bfe35 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { CoreStart } from '../../../../../src/core/public'; -import { API_BASE_PATH, BASE_PATH } from '../../common'; +import { API_BASE_PATH } from '../../common'; import { AppContextProvider, AppDependencies } from './app_context'; import { App } from './app'; @@ -32,7 +32,6 @@ export const renderApp = ( const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, apiBasePath: API_BASE_PATH, - appBasePath: BASE_PATH, trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService), docLinks, toasts: notifications.toasts, diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 51deaf42cc72c..7bd04cdbf0c91 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -150,7 +150,14 @@ export const IndexManagementHome: React.FunctionComponent - + From effd504d0b807f1579fcf7547832edd58a282fd6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 16 Jun 2020 23:12:13 -0600 Subject: [PATCH 38/68] [SIEM] Adds example unit test to convert KQL using a nested query ## Summary Adds example unit test to convert KQL using a nested query ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../signals/get_filter.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index b1500e47db843..61cd9cfedd94f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -462,6 +462,61 @@ describe('get_filter', () => { }, }); }); + + test('it should work with a nested object queries', () => { + const esQuery = getQueryFilter( + 'category:{ name:Frank and trusted:true }', + 'kuery', + [], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + nested: { + path: 'category', + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'category.name': 'Frank', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'category.trusted': true, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); }); describe('getFilter', () => { From 3ee0bf21324841a80e09cb3606b8f9f4887b550d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 17 Jun 2020 10:31:16 +0200 Subject: [PATCH 39/68] Explore underlying data (#68496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 stub discover_enhanced plugin * feat: 🎸 improve view in discover action * feat: 🎸 add URL generator to "View in Discover" action * feat: 🎸 implement navigation and getHref in view raw logs actio * fix: 🐛 disable action in "edit" mode * refactor: 💡 renamce context menu view in discover action * feat: 🎸 rename action to "explore data" * fix: 🐛 correctly generate action path * feat: 🎸 add internationalization to "explore action" * fix: 🐛 correctly parse generated Discover URL path * test: 💍 setup basic functional tests * refactor: 💡 modularize url generation logic * feat: 🎸 export CommonlyUsed type * test: 💍 add test subjects to panel custom time range modal * test: 💍 add index patterna and time range functional tests * refactor: 💡 rename action file * refactor: 💡 use URL generator from Discover plugin's contract * test: 💍 add "Explore raw data" action unit tests * fix: 🐛 import share plugin to check if it is enabled * Update x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts Co-authored-by: Matthias Wilhelm * chore: 🤖 add discover_enhanced to KibanaApp codeowners * test: 💍 improve "Explore underlying data" functional tests * test: 💍 improve
link assertion Co-authored-by: Elastic Machine Co-authored-by: Matthias Wilhelm --- .github/CODEOWNERS | 1 + test/functional/page_objects/discover_page.ts | 17 ++ test/functional/page_objects/time_picker.ts | 30 +- x-pack/.i18nrc.json | 1 + x-pack/plugins/discover_enhanced/kibana.json | 9 + .../discover_enhanced/public/actions/index.ts | 7 + .../explore_data_context_menu_action.test.ts | 209 ++++++++++++++ .../explore_data_context_menu_action.ts | 156 +++++++++++ .../public/actions/view_in_discover/index.ts | 7 + .../plugins/discover_enhanced/public/index.ts | 17 ++ .../discover_enhanced/public/plugin.ts | 61 ++++ .../public/custom_time_range_action.tsx | 5 +- .../public/custom_time_range_badge.tsx | 5 +- .../public/customize_time_range_modal.tsx | 5 +- .../drilldowns/dashboard_drilldowns.ts | 33 +-- .../drilldowns/explore_data_panel_action.ts | 83 ++++++ .../apps/dashboard/drilldowns/index.ts | 17 +- .../dashboard/drilldowns/data.json | 263 ++++++++++++++++++ .../dashboard/drilldowns/data.json.gz | Bin 2662 -> 0 bytes .../services/dashboard/drilldowns_manage.ts | 14 + .../functional/services/dashboard/index.ts | 1 + .../services/dashboard/panel_time_range.ts | 56 ++++ x-pack/test/functional/services/index.ts | 2 + 23 files changed, 963 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/discover_enhanced/kibana.json create mode 100644 x-pack/plugins/discover_enhanced/public/actions/index.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts create mode 100644 x-pack/plugins/discover_enhanced/public/index.ts create mode 100644 x-pack/plugins/discover_enhanced/public/plugin.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts create mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/data.json delete mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz create mode 100644 x-pack/test/functional/services/dashboard/panel_time_range.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 681564b44297f..e6f6e83253c8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,7 @@ # App /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app +/x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 4e2e06eaecf96..9ba3c9c1c2c88 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -324,6 +324,23 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider const nr = await el.getAttribute('data-fetch-counter'); return Number(nr); } + + /** + * Check if Discover app is currently rendered on the screen. + */ + public async isDiscoverAppOnScreen(): Promise { + const result = await find.allByCssSelector('discover-app'); + return result.length === 1; + } + + /** + * Wait until Discover app is rendered on the screen. + */ + public async waitForDiscoverAppOnScreen() { + await retry.waitFor('Discover app on screen', async () => { + return await this.isDiscoverAppOnScreen(); + }); + } } return new DiscoverPage(); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index b7c1599fd48c5..20ae89fc1a8d0 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -21,6 +21,18 @@ import moment from 'moment'; import { FtrProviderContext } from '../ftr_provider_context.d'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; +export type CommonlyUsed = + | 'Today' + | 'This_week' + | 'Last_15 minutes' + | 'Last_30 minutes' + | 'Last_1 hour' + | 'Last_24 hours' + | 'Last_7 days' + | 'Last_30 days' + | 'Last_90 days' + | 'Last_1 year'; + export function TimePickerProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); @@ -30,18 +42,6 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const { header, common } = getPageObjects(['header', 'common']); const kibanaServer = getService('kibanaServer'); - type CommonlyUsed = - | 'Today' - | 'This_week' - | 'Last_15 minutes' - | 'Last_30 minutes' - | 'Last_1 hour' - | 'Last_24 hours' - | 'Last_7 days' - | 'Last_30 days' - | 'Last_90 days' - | 'Last_1 year'; - class TimePicker { defaultStartTime = 'Sep 19, 2015 @ 06:31:44.000'; defaultEndTime = 'Sep 23, 2015 @ 18:31:44.000'; @@ -227,6 +227,12 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo }; } + public async getShowDatesButtonText() { + const button = await testSubjects.find('superDatePickerShowDatesButton'); + const text = await button.getVisibleText(); + return text; + } + public async getTimeDurationForSharing() { return await testSubjects.getAttribute( 'dataSharedTimefilterDuration', diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 85b40d33c4089..21b2df3ba12f8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,6 +9,7 @@ "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], "xpack.canvas": "plugins/canvas", "xpack.dashboard": "plugins/dashboard_enhanced", + "xpack.discover": "plugins/discover_enhanced", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json new file mode 100644 index 0000000000000..25b2a83baf98f --- /dev/null +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "discoverEnhanced", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["uiActions", "embeddable", "discover"], + "optionalPlugins": ["share"] +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/index.ts b/x-pack/plugins/discover_enhanced/public/actions/index.ts new file mode 100644 index 0000000000000..cbb955fa46340 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './view_in_discover'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts new file mode 100644 index 0000000000000..a7167d2e2e691 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ExploreDataContextMenuAction, + ACTION_EXPLORE_DATA, + Params, + PluginDeps, +} from './explore_data_context_menu_action'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { i18n } from '@kbn/i18n'; +import { + VisualizeEmbeddableContract, + VISUALIZE_EMBEDDABLE_TYPE, +} from '../../../../../../src/plugins/visualizations/public'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; + +const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; + +jest.mock('@kbn/i18n', () => ({ + i18n: { + translate: jest.fn((key, options) => options.defaultMessage), + }, +})); + +afterEach(() => { + i18nTranslateSpy.mockClear(); +}); + +const setup = () => { + type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + const core = coreMock.createStart(); + + const urlGenerator: UrlGenerator = ({ + id: ACTION_EXPLORE_DATA, + createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), + } as unknown) as UrlGenerator; + + const plugins: PluginDeps = { + discover: { + urlGenerator, + }, + }; + + const params: Params = { + start: () => ({ + plugins, + self: {}, + core, + }), + }; + const action = new ExploreDataContextMenuAction(params); + + const input = { + viewMode: ViewMode.VIEW, + }; + + const output = { + indexPatterns: [ + { + id: 'index-ptr-foo', + }, + ], + }; + + const embeddable: VisualizeEmbeddableContract = ({ + type: VISUALIZE_EMBEDDABLE_TYPE, + getInput: () => input, + getOutput: () => output, + } as unknown) as VisualizeEmbeddableContract; + + const context = { + embeddable, + }; + + return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; +}; + +describe('"Explore underlying data" panel action', () => { + test('action has Discover icon', () => { + const { action } = setup(); + expect(action.getIconType()).toBe('discoverApp'); + }); + + test('title is "Explore underlying data"', () => { + const { action } = setup(); + expect(action.getDisplayName()).toBe('Explore underlying data'); + }); + + test('translates title', () => { + expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); + + setup().action.getDisplayName(); + + expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); + expect(i18nTranslateSpy.mock.calls[0][0]).toBe( + 'xpack.discover.FlyoutCreateDrilldownAction.displayName' + ); + }); + + describe('isCompatible()', () => { + test('returns true when all conditions are met', async () => { + const { action, context } = setup(); + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(true); + }); + + test('returns false when URL generator is not present', async () => { + const { action, plugins, context } = setup(); + (plugins.discover as any).urlGenerator = undefined; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable is not Visualize embeddable', async () => { + const { action, embeddable, context } = setup(); + (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable does not have index patterns', async () => { + const { action, output, context } = setup(); + delete output.indexPatterns; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable index patterns are empty', async () => { + const { action, output, context } = setup(); + output.indexPatterns = []; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if dashboard is in edit mode', async () => { + const { action, input, context } = setup(); + input.viewMode = ViewMode.EDIT; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + }); + + describe('getHref()', () => { + test('returns URL path generated by URL generator', async () => { + const { action, context } = setup(); + + const href = await action.getHref(context); + + expect(href).toBe('/xyz/app/discover/foo#bar'); + }); + + test('calls URL generator with right arguments', async () => { + const { action, urlGenerator, context } = setup(); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + indexPatternId: 'index-ptr-foo', + }); + }); + }); + + describe('execute()', () => { + test('calls platform SPA navigation method', async () => { + const { action, context, core } = setup(); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(0); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + }); + + test('calls platform SPA navigation method with right arguments', async () => { + const { action, context, core } = setup(); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + expect(core.application.navigateToApp.mock.calls[0]).toEqual([ + 'discover', + { + path: '/foo#bar', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts new file mode 100644 index 0000000000000..d66ca129934a8 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ + +import { i18n } from '@kbn/i18n'; +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; +import { + EmbeddableContext, + IEmbeddable, + ViewMode, +} from '../../../../../../src/plugins/embeddable/public'; +import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { CoreStart } from '../../../../../../src/core/public'; +import { + VisualizeEmbeddableContract, + VISUALIZE_EMBEDDABLE_TYPE, +} from '../../../../../../src/plugins/visualizations/public'; + +// TODO: Replace this logic with KibanaURL once it is available. +// https://github.com/elastic/kibana/issues/64497 +class KibanaURL { + public readonly path: string; + public readonly appName: string; + public readonly appPath: string; + + constructor(path: string) { + const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); + + if (!match) { + throw new Error('Unexpected Discover URL path.'); + } + + const [, appName, appPath] = match; + + if (!appName || !appPath) { + throw new Error('Could not parse Discover URL path.'); + } + + this.path = path; + this.appName = appName; + this.appPath = appPath; + } +} + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +const isOutputWithIndexPatterns = ( + output: unknown +): output is { indexPatterns: Array<{ id: string }> } => { + if (!output || typeof output !== 'object') return false; + return Array.isArray((output as any).indexPatterns); +}; + +const isVisualizeEmbeddable = ( + embeddable: IEmbeddable +): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE; + +export interface PluginDeps { + discover: Pick; +} + +export interface CoreDeps { + application: Pick; +} + +export interface Params { + start: StartServicesGetter; +} + +export class ExploreDataContextMenuAction implements Action { + public readonly id = ACTION_EXPLORE_DATA; + + public readonly type = ACTION_EXPLORE_DATA; + + public readonly order = 200; + + constructor(private readonly params: Params) {} + + public getDisplayName() { + return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Explore underlying data', + }); + } + + public getIconType() { + return 'discoverApp'; + } + + public async isCompatible({ embeddable }: EmbeddableContext) { + if (!this.params.start().plugins.discover.urlGenerator) return false; + if (!isVisualizeEmbeddable(embeddable)) return false; + if (!this.getIndexPattern(embeddable)) return false; + if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; + return true; + } + + public async execute({ embeddable }: EmbeddableContext) { + if (!isVisualizeEmbeddable(embeddable)) return; + + const { core } = this.params.start(); + const { appName, appPath } = await this.getUrl(embeddable); + + await core.application.navigateToApp(appName, { + path: appPath, + }); + } + + public async getHref({ embeddable }: EmbeddableContext): Promise { + if (!isVisualizeEmbeddable(embeddable)) { + throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`); + } + + const { path } = await this.getUrl(embeddable); + + return path; + } + + private async getUrl(embeddable: VisualizeEmbeddableContract): Promise { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { timeRange, query, filters } = embeddable.getInput(); + const indexPatternId = this.getIndexPattern(embeddable); + + const path = await urlGenerator.createUrl({ + indexPatternId, + filters, + query, + timeRange, + }); + + return new KibanaURL(path); + } + + /** + * @returns Returns empty string if no index pattern ID found. + */ + private getIndexPattern(embeddable: VisualizeEmbeddableContract): string { + const output = embeddable!.getOutput(); + + if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { + return output.indexPatterns[0].id; + } + + return ''; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts new file mode 100644 index 0000000000000..8788621365385 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './explore_data_context_menu_action'; diff --git a/x-pack/plugins/discover_enhanced/public/index.ts b/x-pack/plugins/discover_enhanced/public/index.ts new file mode 100644 index 0000000000000..943a212dd7c4e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { DiscoverEnhancedPlugin } from './plugin'; + +export { + DiscoverEnhancedPlugin, + DiscoverEnhancedSetupDependencies, + DiscoverEnhancedStartDependencies, +} from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new DiscoverEnhancedPlugin(initializerContext); diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts new file mode 100644 index 0000000000000..f55c5dab3449b --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext } from 'kibana/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; +import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; +import { + EmbeddableSetup, + EmbeddableStart, + EmbeddableContext, + CONTEXT_MENU_TRIGGER, +} from '../../../../src/plugins/embeddable/public'; +import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions'; + +declare module '../../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_EXPLORE_DATA]: EmbeddableContext; + } +} + +export interface DiscoverEnhancedSetupDependencies { + discover: DiscoverSetup; + embeddable: EmbeddableSetup; + share?: SharePluginSetup; + uiActions: UiActionsSetup; +} + +export interface DiscoverEnhancedStartDependencies { + discover: DiscoverStart; + embeddable: EmbeddableStart; + share?: SharePluginStart; + uiActions: UiActionsStart; +} + +export class DiscoverEnhancedPlugin + implements + Plugin { + constructor(public readonly initializerContext: PluginInitializerContext) {} + + setup( + core: CoreSetup, + { uiActions, share }: DiscoverEnhancedSetupDependencies + ) { + const start = createStartServicesGetter(core.getStartServices); + + if (!!share) { + const exploreDataAction = new ExploreDataContextMenuAction({ start }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction); + } + } + + start(core: CoreStart, plugins: DiscoverEnhancedStartDependencies) {} + + stop() {} +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 4da4d648bc0ec..5d9804d2a5c33 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -103,7 +103,10 @@ export class CustomTimeRangeAction implements ActionByType + />, + { + 'data-test-subj': 'customizeTimeRangeModal', + } ); } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx index 59a2fc27267b0..28d99db9b3ef7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx @@ -82,7 +82,10 @@ export class CustomTimeRangeBadge implements ActionByType + />, + { + 'data-test-subj': 'customizeTimeRangeModal', + } ); } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx b/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx index 8a9eeacb66564..993720775a29f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx @@ -101,7 +101,7 @@ export class CustomizeTimeRangeModal extends Component - + - +
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts index 2c63baf7c75cb..bcdd3d1f82e7d 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -7,9 +7,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -const DASHBOARD_WITH_PIE_CHART_NAME = 'Dashboard with Pie Chart'; -const DASHBOARD_WITH_AREA_CHART_NAME = 'Dashboard With Area Chart'; - const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; @@ -18,8 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); - const kibanaServer = getService('kibanaServer'); - const esArchiver = getService('esArchiver'); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -30,19 +25,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Dashboard Drilldowns', function () { before(async () => { log.debug('Dashboard Drilldowns:initTests'); - await esArchiver.loadIfNeeded('logstash_functional'); - await esArchiver.load('dashboard/drilldowns'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); }); - after(async () => { - await esArchiver.unload('dashboard/drilldowns'); - }); - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode(DASHBOARD_WITH_PIE_CHART_NAME); + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); // create drilldown await dashboardPanelActions.openContextMenu(); @@ -51,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: DASHBOARD_WITH_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, }); await dashboardDrilldownsManage.saveChanges(); await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); @@ -60,10 +50,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard(DASHBOARD_WITH_PIE_CHART_NAME, { - saveAsNew: false, - waitDialogIsClosed: true, - }); + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); // trigger drilldown action by clicking on a pie and picking drilldown action by it's name await pieChart.filterOnPieSlice('40,000'); @@ -117,7 +110,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_WITH_AREA_CHART_NAME); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); await brushAreaChart(); await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts new file mode 100644 index 0000000000000..24d6e820ac0eb --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ACTION_ID = 'ACTION_EXPLORE_DATA'; +const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const drilldowns = getService('dashboardDrilldownsManage'); + const { dashboard, discover, common, timePicker } = getPageObjects([ + 'dashboard', + 'discover', + 'common', + 'timePicker', + ]); + const panelActions = getService('dashboardPanelActions'); + const panelActionsTimeRange = getService('dashboardPanelTimeRange'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + describe('Explore underlying data - panel action', function () { + before(async () => { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + }); + + after(async () => { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + }); + + it('action exists in panel context menu', async () => { + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await panelActions.openContextMenu(); + await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + }); + + it('is a link element', async () => { + const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + const tag = await actionElement.getTagName(); + + expect(tag.toLowerCase()).to.be('a'); + }); + + it('navigates to Discover app to index pattern of the panel on action click', async () => { + await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + + const el = await testSubjects.find('indexPattern-switch-link'); + const text = await el.getVisibleText(); + + expect(text).to.be('logstash-*'); + }); + + it('carries over panel time range', async () => { + await common.navigateToApp('dashboard'); + + await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + + await panelActions.openContextMenu(); + await panelActionsTimeRange.clickTimeRangeActionInContextMenu(); + await panelActionsTimeRange.clickToggleQuickMenuButton(); + await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days'); + await panelActionsTimeRange.clickModalPrimaryButton(); + + await dashboard.saveDashboard('Dashboard with Pie Chart'); + + await panelActions.openContextMenu(); + await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + + const text = await timePicker.getShowDatesButtonText(); + const lowercaseText = text.toLowerCase(); + + expect(lowercaseText.includes('last 90 days')).to.be(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index d1081b053a8b9..19d85ad0e448f 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -5,9 +5,24 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { +export default function ({ loadTestFile, getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + describe('drilldowns', function () { this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('dashboard/drilldowns'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + }); + + after(async () => { + await esArchiver.unload('dashboard/drilldowns'); + }); + loadTestFile(require.resolve('./dashboard_drilldowns')); + loadTestFile(require.resolve('./explore_data_panel_action')); }); } diff --git a/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json new file mode 100644 index 0000000000000..220daaeb61d0e --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json @@ -0,0 +1,263 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "description": "This is the default space!", + "name": "Default" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization☺漢字-DataTable", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "DataTable", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization☺漢字 DataTable", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"histogram\",\"schema\":\"bucket\",\"params\":{\"field\":\"bytes\",\"interval\":2000,\"extended_bounds\":{}}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization☺-VerticalBarChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "VerticalBarChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization☺ VerticalBarChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"histogram\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"scale\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization-TileMap", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "TileMap", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization TileMap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":true,\"addTooltip\":true,\"heatMaxZoom\":16,\"heatMinOpacity\":0.1,\"heatRadius\":25,\"heatBlur\":15,\"heatNormalizeData\":true,\"wms\":{\"enabled\":false,\"url\":\"https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer\",\"options\":{\"version\":\"1.3.0\",\"layers\":\"0\",\"format\":\"image/png\",\"transparent\":true,\"attribution\":\"Maps provided by USGS\",\"styles\":\"\"}}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"precision\":2}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash*", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash*" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization-PieChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "PieChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization PieChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"pie\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"isDonut\":false},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"memory\",\"interval\":40000,\"extended_bounds\":{}}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization漢字-LineChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "LineChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization漢字 LineChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"line\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"showCircles\":true,\"smoothLines\":false,\"interpolate\":\"linear\",\"scale\":\"linear\",\"drawLinesBetweenPoints\":true,\"radiusRatio\":9,\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"extension.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"row\":false}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization-MetricChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "MetricChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization MetricChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"metric\",\"params\":{\"handleNoResults\":true,\"fontSize\":60},\"aggs\":[{\"id\":\"1\",\"type\":\"percentile_ranks\",\"schema\":\"metric\",\"params\":{\"field\":\"memory\",\"values\":[99]}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:Visualization漢字-AreaChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization漢字 AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:24f3f950-69d9-11ea-a14d-e341629a29e6", + "index": ".kibana", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.7.0\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"e637d5f0-a7e6-4635-81ed-39f2b1aac6f4\"},\"panelIndex\":\"e637d5f0-a7e6-4635-81ed-39f2b1aac6f4\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"ffd3e4dc-cb1a-419f-afeb-03e8c7742bbf\",\"triggers\":[\"VALUE_CLICK_TRIGGER\",\"SELECT_RANGE_TRIGGER\"],\"action\":{\"name\":\"Go to pie chart dashboard\",\"config\":{\"dashboardId\":\"41e77910-69d9-11ea-a14d-e341629a29e6\",\"useCurrentDateRange\":true,\"useCurrentFilters\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", + "refreshInterval": { "pause": true, "value": 0 }, + "timeFrom": "2015-09-19T17:34:10.297Z", + "timeRestore": true, + "timeTo": "2015-09-23T00:09:17.180Z", + "title": "Dashboard With Area Chart", + "version": 1 + }, + "references": [ + { + "id": "Visualization漢字-AreaChart", + "name": "panel_0", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-03-19T11:59:53.701Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:41e77910-69d9-11ea-a14d-e341629a29e6", + "index": ".kibana", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.7.0\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"8c5df6b2-0cc9-4887-a2d9-6a9a192f3407\"},\"panelIndex\":\"8c5df6b2-0cc9-4887-a2d9-6a9a192f3407\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"}]", + "refreshInterval": { "pause": true, "value": 0 }, + "timeFrom": "2015-09-19T17:34:10.297Z", + "timeRestore": true, + "timeTo": "2015-09-23T00:09:17.180Z", + "title": "Dashboard with Pie Chart", + "version": 1 + }, + "references": [ + { + "id": "Visualization-PieChart", + "name": "panel_0", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-03-19T11:59:53.701Z" + } + } +} + + diff --git a/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz deleted file mode 100644 index a9b23ca7a579bdd2de633c67e386a31313162e2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2662 zcmV-s3YqmEiwFoonWQOZ*BnXT-|cpxD~$7Q?R=1L^3JKlBKISwl{U_ z*d9rn%*OU`AQBYOkOY?iEh`>9M|<7-zCfo>)0y@;dH{kHDanqr>jV7i;;teF7{9waCx6-id=jrT3esV z6h$bfGnyvOstZFsULpX=#mq@a%n6M|7ZaZ_1O9Oz8)_IsKJ`1*t9&Rzp=9=0F``)tU;pu+fBx;?fB#GK7;!W~(*S?F zK5{N9;}n9fksnjrIuG)mZ1gd@#qP!Q&)DJbF-Mhd1XCC#jz5;H{c(J8E_%Da&Sbc5 z46hpjoiUD>-~9b`~94$-ybHW3up6!Y^whEn+F-LqQ%Cg*eixZY<%qmj}lg}v8<*-tS zb38XHV@sig(PT3hV@pzu5)`sb2Qf}A(M;T7nb5?1I=@5-Cny-vxwywe0mG^VEJ7T1 z$Pa_y4Bz6QXcrnnvzfTQ66SE_h*p&Who%viB#9x zxO2UyY3h8&d_zEw;2`U0y5N|}b`pUtA%uY3HccTnzT+5t$hD`yq^p=%-atzLF8bsvxjj3fIFshiQj6c9pg#s>+p4CfowJ6yj<$!Dq691~Q^ z`{_hP53isF-bP(0rtgXzIP9o6-6t{#6MVl>4kw?fSsbHnO zuPASWRH)a=ug_@`3ZbwlpH{=na{+K9${M!>GHH^l*(ZaoR;OjkTLQGm zV)wd{5j?>@mjn_*owXMWuM?(8Pz2?EBC)4XC;4wom zz6fwO&~;O)}JvxKiH0lC_;yIF^i;(>DLoUt2-}F4L8*!adpJ?9RcJTIrPqbIhO#V|pjmNqIbbCk zGuSHMBl3=vJFgU^T?-DtACOikFMVAt0vXzq8vHBQx|5ycNpQ= z7VX+!jedqEuAPgA-aRxLP0n*@R*YQQ*O`qUNCd)tYcjW>Q8{)tp;zUgOnfp6Ipu3= zc=~jh_K23aqO*<^U1-*VT(M)iCz&+YlJ+x|0?!vCWfZF)w$@{AauZhcmQdT4fXb^& zc1(#{me_f{0-DhbG;&r+aaA9u$vs*wIBQQx8>=(5;EKgGV!GLQ6Jo~n7Y+FY7_Cvo zyEds_AI367)@-pmrqfh%s#1FdZ^aeOnUW=r<|Q-zzF8FCwLvxAC@&_rH7~E!FIs_* z8Z2-_z-pgns-11h%YwRBv+Fc}yyZ=?cD5@fxa+4j-3dCcw(F;vI?NodeK@aU?P|5< zN7w3W_ExC-GZp!1nn#jt-=@{)lh4{gUu|JIf0vEZX+W5-dx_Xy|I}m0v%>cCe4qPk zIA9J<@UKUjZ)4%6QQ3GB;vdAP=I3g=@d|$%+xDlFzIK0bLvr=2_V2O@>5^dG`yAI; zc*)z4wu!pKeMm9MV&s{&GZJ=8qm-{gQvWXGuWx}VE#sB;5_ZixZA4aQ1)PycdvLDv z@PeaL9Tc!t;vEL5*c$wtQ7TRj$Tk(0o@F7W+!vyjHwepsh-n~8mtkd>^46`(E>vZ* zkGGi5F^(?9(qUR+0HG`%?wgEqJKpq31&8bvIzKH(BeoLCTQVrKK z24$-xytWG+X*8t_=FJ`qHszfjE3#5*YO7XX0tbN2TjYusEMqu+r*u& z)oTw>dw_f2jz&fQk3vm3qRm zGU5F*5|x6~CoaB35e_CI{#WxpWnp8{UUYP|b~Hu5)mKMTi1QlM85(epsNvdOF~qex zY(v1RcmZ9g|4CLa_Bwrc-!q%2k9*B-ue0Agu&~?g47~Q#LdfZP-Arc5$7zAjPkT^! zYBzdx`ZjUd?x zc*{|mh|@#Brg#K?h*xg2YWzVu;%d?sGy@z%p`yk+J{Dk%w z#m94Tw#ZF(yoj{#2z%$n?lwWN!MUo_kO}q@%={dvM2rb11)kuiWN?Ke=2~J6@ec_N zg&(&~YrkoN3I-FaKkRgemf30#`k&S(JfZXhaf+_1jT2h#s@<8G=Fl7rt$xcoFxR?L zIsG^{i1CqdU#4?oBcGR2_QuR Date: Wed, 17 Jun 2020 12:10:28 +0200 Subject: [PATCH 40/68] [APM] Only add decimals for numbers below 10 (#69334) --- .../apm/e2e/cypress/integration/snapshots.js | 6 +- .../ServiceList/__test__/List.test.js | 2 +- .../__snapshots__/CustomPlot.test.js.snap | 6 +- .../Histogram/__test__/Histogram.test.js | 10 +- .../__snapshots__/Histogram.test.js.snap | 24 ++-- .../Timeline/Marker/ErrorMarker.test.tsx | 2 +- .../formatters/__test__/duration.test.ts | 12 +- .../apm/public/utils/formatters/duration.ts | 119 +++++++++++------- 8 files changed, 102 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index 2fae8bdab2b30..d4c8ba4910850 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,9 +1,9 @@ module.exports = { APM: { 'Transaction duration charts': { - '1': '350.0 ms', - '2': '175.0 ms', - '3': '0.0 ms', + '1': '350 ms', + '2': '175 ms', + '3': '0 ms', }, }, __version: '4.5.0', diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 09fef5da16ae7..927779b571fd8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -40,7 +40,7 @@ describe('ServiceOverview -> List', () => { expect(renderedColumns[0]).toMatchSnapshot(); expect(renderedColumns.slice(2)).toEqual([ 'python', - '91.5 ms', + '92 ms', '86.9 tpm', '12.6 err.', ]); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index ba3174156d5d4..6c5a200fb6f27 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -9,7 +9,7 @@ Array [ "text": Avg. - 467.6 ms + 468 ms , }, @@ -2744,7 +2744,7 @@ Array [ - 467.6 ms + 468 ms @@ -5923,7 +5923,7 @@ Array [ - 467.6 ms + 468 ms diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index f84b0cfeda369..7eaeecca31b36 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -11,8 +11,8 @@ import d3 from 'd3'; import { HistogramInner } from '../index'; import response from './response.json'; import { - asDecimal, getDurationFormatter, + asInteger, } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; @@ -33,8 +33,8 @@ describe('Histogram', () => { transactionId="myTransactionId" onClick={onClick} formatX={(time) => timeFormatter(time).formatted} - formatYShort={(t) => `${asDecimal(t)} occ.`} - formatYLong={(t) => `${asDecimal(t)} occurrences`} + formatYShort={(t) => `${asInteger(t)} occ.`} + formatYLong={(t) => `${asInteger(t)} occurrences`} tooltipHeader={(bucket) => { const xFormatted = timeFormatter(bucket.x); const x0Formatted = timeFormatter(bucket.x0); @@ -78,9 +78,9 @@ describe('Histogram', () => { const tooltips = wrapper.find('Tooltip'); expect(tooltips.length).toBe(1); - expect(tooltips.prop('header')).toBe('811.1 - 926.9 ms'); + expect(tooltips.prop('header')).toBe('811 - 927 ms'); expect(tooltips.prop('tooltipPoints')).toEqual([ - { value: '49.0 occurrences' }, + { value: '49 occurrences' }, ]); expect(tooltips.prop('x')).toEqual(869010); expect(tooltips.prop('y')).toEqual(27.5); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index ff38ae4b563dd..1f935af7c8999 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -127,7 +127,7 @@ exports[`Histogram Initially should have default markup 1`] = ` textAnchor="middle" transform="translate(0, 18)" > - 0.0 ms + 0 ms - 500.0 ms + 500 ms - 1,000.0 ms + 1,000 ms - 1,500.0 ms + 1,500 ms - 2,000.0 ms + 2,000 ms - 2,500.0 ms + 2,500 ms - 3,000.0 ms + 3,000 ms @@ -371,7 +371,7 @@ exports[`Histogram Initially should have default markup 1`] = ` textAnchor="end" transform="translate(-8, 0)" > - 0.0 occ. + 0 occ. - 27.5 occ. + 28 occ. - 55.0 occ. + 55 occ. @@ -1477,7 +1477,7 @@ exports[`Histogram when hovering over a non-empty bucket should have correct mar
- 811.1 - 926.9 ms + 811 - 927 ms
@@ -1488,7 +1488,7 @@ exports[`Histogram when hovering over a non-empty bucket should have correct mar
- 49.0 occurrences + 49 occurrences
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index 2b6f0c7aa1319..7dc68910c6bed 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -37,7 +37,7 @@ describe('ErrorMarker', () => { act(() => { fireEvent.click(component.getByTestId('popover')); }); - expectTextsInDocument(component, ['10.0 ms']); + expectTextsInDocument(component, ['10 ms']); return component; } function getKueryDecoded(url: string) { diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts index 6d4b65d2aa9b4..c4d59beb4b7a2 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts +++ b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -14,14 +14,14 @@ describe('duration formatters', () => { expect(asDuration(1)).toEqual('1 μs'); expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs'); expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual( - '1,000.0 ms' + '1,000 ms' ); expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual( - '10,000.0 ms' + '10,000 ms' ); - expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s'); - expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min'); - expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60.0 min'); + expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20 s'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10 min'); + expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60 min'); expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); }); @@ -41,7 +41,7 @@ describe('duration formatters', () => { describe('asMilliseconds', () => { it('converts to formatted decimal milliseconds', () => { - expect(asMillisecondDuration(0)).toEqual('0.0 ms'); + expect(asMillisecondDuration(0)).toEqual('0 ms'); }); }); }); diff --git a/x-pack/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts index a603faab37538..64a9e3b952b98 100644 --- a/x-pack/plugins/apm/public/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts @@ -18,13 +18,6 @@ interface FormatterOptions { type DurationTimeUnit = TimeUnit | 'microseconds'; -interface DurationUnit { - [unit: string]: { - label: string; - convert: (value: number) => string; - }; -} - interface ConvertedDuration { value: string; unit?: string; @@ -38,42 +31,69 @@ export type TimeFormatter = ( type TimeFormatterBuilder = (max: number) => TimeFormatter; -const durationUnit: DurationUnit = { - hours: { - label: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { - defaultMessage: 'h', - }), - convert: (value: number) => - asDecimal(moment.duration(value / 1000).asHours()), - }, - minutes: { - label: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { - defaultMessage: 'min', - }), - convert: (value: number) => - asDecimal(moment.duration(value / 1000).asMinutes()), - }, - seconds: { - label: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { - defaultMessage: 's', - }), - convert: (value: number) => - asDecimal(moment.duration(value / 1000).asSeconds()), - }, - milliseconds: { - label: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { - defaultMessage: 'ms', - }), - convert: (value: number) => - asDecimal(moment.duration(value / 1000).asMilliseconds()), - }, - microseconds: { - label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { - defaultMessage: 'μs', - }), - convert: (value: number) => asInteger(value), - }, -}; +function asDecimalOrInteger(value: number) { + // exact 0 or above 10 should not have decimal + if (value === 0 || value >= 10) { + return asInteger(value); + } + return asDecimal(value); +} + +function getUnitLabelAndConvertedValue( + unitKey: DurationTimeUnit, + value: number +) { + switch (unitKey) { + case 'hours': { + return { + unitLabel: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h', + }), + convertedValue: asDecimalOrInteger( + moment.duration(value / 1000).asHours() + ), + }; + } + case 'minutes': { + return { + unitLabel: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min', + }), + convertedValue: asDecimalOrInteger( + moment.duration(value / 1000).asMinutes() + ), + }; + } + case 'seconds': { + return { + unitLabel: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { + defaultMessage: 's', + }), + convertedValue: asDecimalOrInteger( + moment.duration(value / 1000).asSeconds() + ), + }; + } + case 'milliseconds': { + return { + unitLabel: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { + defaultMessage: 'ms', + }), + convertedValue: asDecimalOrInteger( + moment.duration(value / 1000).asMilliseconds() + ), + }; + } + case 'microseconds': { + return { + unitLabel: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { + defaultMessage: 'μs', + }), + convertedValue: asInteger(value), + }; + } + } +} /** * Converts a microseconds value into the unit defined. @@ -87,16 +107,19 @@ function convertTo({ microseconds: Maybe; defaultValue?: string; }): ConvertedDuration { - const duration = durationUnit[unit]; - if (!duration || microseconds == null) { + if (microseconds == null) { return { value: defaultValue, formatted: defaultValue }; } - const convertedValue = duration.convert(microseconds); + const { convertedValue, unitLabel } = getUnitLabelAndConvertedValue( + unit, + microseconds + ); + return { value: convertedValue, - unit: duration.label, - formatted: `${convertedValue} ${duration.label}`, + unit: unitLabel, + formatted: `${convertedValue} ${unitLabel}`, }; } From ab1270e566f878dfc5e9ae226fe42cccb1ff92d6 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 17 Jun 2020 14:27:05 +0300 Subject: [PATCH 41/68] Replaces the Custom Color Picker on TSVB with the EuiColorPicker (#68888) * Replace the Custom Color Picker on TSVB with the EuiColorPicker * Remove the custom picker sass * Remove private modules of eui and the custom color swatches * Clear the color * changes in test implementation Co-authored-by: Elastic Machine --- .../application/components/_color_picker.scss | 33 --- .../components/_custom_color_picker.scss | 97 --------- .../public/application/components/_index.scss | 1 - .../application/components/color_picker.js | 125 ----------- .../components/color_picker.test.js | 69 ------ .../components/color_picker.test.tsx | 61 ++++++ .../application/components/color_picker.tsx | 95 +++++++++ .../components/custom_color_picker.js | 200 ------------------ .../page_objects/visual_builder_page.ts | 4 +- 9 files changed, 158 insertions(+), 527 deletions(-) delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/color_picker.js delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/color_picker.test.js create mode 100644 src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx create mode 100644 src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/custom_color_picker.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss index e486f56fe0fe8..9d50b0875dd0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss @@ -8,39 +8,6 @@ position: relative; } -.tvbColorPicker__swatch-empty, -.tvbColorPicker__swatch { - // SASSTODO: Replace with EUI component - // sass-lint:disable-block placeholder-in-extend - @extend .euiColorPickerSwatch; -} - -.tvbColorPicker__swatch-empty { - background-color: transparent; - background-size: 22px 22px; - background-image: repeating-linear-gradient( - -45deg, - $euiColorDanger, - $euiColorDanger 2px, - transparent 2px, - transparent $euiSize - ); -} - .tvbColorPicker__clear { margin-left: $euiSizeXS; } - -.tvbColorPicker__popover { - position: absolute; - top: $euiSizeL; - z-index: 2; -} - -.tvbColorPicker__cover { - position: fixed; - top: 0; - right: 0; - left: 0; - bottom: 0; -} diff --git a/src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss deleted file mode 100644 index f2f5eff35fc5d..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss +++ /dev/null @@ -1,97 +0,0 @@ -// EUITODO: Convert to EuiColorPicker -// with additional support for alpha, saturation, swatches - -// SASSTODO: This custom picker moved all styles from react-color inline styles -// to SASS, but it should be in EUI. -// Also, some pixel values were kept as is to match inline styles from react-color -.tvbColorPickerPopUp { - @include euiBottomShadowMedium; - background-color: $euiColorEmptyShade; - border-radius: $euiBorderRadius; - box-sizing: initial; - width: 275px; - font-family: 'Menlo'; -} - -.tvbColorPickerPopUp__saturation { - width: 100%; - padding-bottom: 55%; - position: relative; - border-radius: $euiBorderRadius $euiBorderRadius 0 0; - overflow: hidden; -} - -.tvbColorPickerPopUp__body { - padding: $euiSize; -} - -.tvbColorPickerPopUp__controls { - display: flex; -} - -.tvbColorPickerPopUp__color { - width: $euiSizeXL; - - // The color indicator doesn't work, hiding it until it does - display: none; -} - -.tvbColorPickerPopUp__color-disableAlpha { - width: $euiSizeL; -} - -.tvbColorPickerPopUp__swatch { - margin-top: 6px; - width: $euiSize; - height: $euiSize; - border-radius: $euiSizeS; - position: relative; - overflow: hidden; -} - -.tvbColorPickerPopUp__swatch-disableAlpha { - width: 10px; - height: 10px; - margin: 0; -} - -.tvbColorPickerPopUp__active { - @include euiBottomShadowMedium; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: $euiSizeS; - z-index: 2; -} - -.tvbColorPickerPopUp__toggles { - flex: 1; -} - -.tvbColorPickerPopUp__hue { - height: 10px; - position: relative; - margin-bottom: $euiSizeS; -} - -.tvbColorPickerPopUp__hue-disableAlpha { - margin-bottom: 0; -} - -.tvbColorPickerPopUp__alpha { - height: 10px; - position: relative; -} - -.tvbColorPickerPopUp__alpha-disableAlpha { - display: none; -} - -.tvbColorPickerPopUp__swatches { - display: flex; - flex-wrap: wrap; - justify-content: center; - margin-top: $euiSize; -} diff --git a/src/plugins/vis_type_timeseries/public/application/components/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/_index.scss index 3a10b07c23e2b..4ee5c1863946b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/_index.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/_index.scss @@ -1,7 +1,6 @@ @import './annotations_editor'; @import './color_rules'; @import './color_picker'; -@import './custom_color_picker'; @import './error'; @import './no_data'; @import './markdown_editor'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.js b/src/plugins/vis_type_timeseries/public/application/components/color_picker.js deleted file mode 100644 index f12797518cd28..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ -// The color picker is not yet accessible. - -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { EuiIconTip } from '@elastic/eui'; -import { CustomColorPicker } from './custom_color_picker'; -import { i18n } from '@kbn/i18n'; - -export class ColorPicker extends Component { - constructor(props) { - super(props); - this.state = { - displayPicker: false, - color: {}, - }; - } - - handleChange = (color) => { - const { rgb } = color; - const part = {}; - part[this.props.name] = `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`; - if (this.props.onChange) this.props.onChange(part); - }; - - handleClick = () => { - this.setState({ displayPicker: !this.state.displayColorPicker }); - }; - - handleClose = () => { - this.setState({ displayPicker: false }); - }; - - handleClear = () => { - const part = {}; - part[this.props.name] = null; - this.props.onChange(part); - }; - - renderSwatch() { - if (!this.props.value) { - return ( - diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx index e85605e42981c..a5bcec1501ad3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx @@ -12,10 +12,14 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; const SETUP_INSTRUCTIONS_LABEL = i18n.translate( 'xpack.apm.setupInstructionsButtonLabel', { - defaultMessage: 'Setup Instructions', + defaultMessage: 'Setup instructions', } ); +const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', { + defaultMessage: 'Add data', +}); + // renders a filled button or a link as a kibana link to setup instructions export function SetupInstructionsLink({ buttonFill = false, @@ -30,8 +34,8 @@ export function SetupInstructionsLink({ {SETUP_INSTRUCTIONS_LABEL} ) : ( - - {SETUP_INSTRUCTIONS_LABEL} + + {ADD_DATA_LABEL} )} diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 14c53557ba2c7..78b7f86993cbd 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, Switch } from 'react-router-dom'; @@ -32,6 +32,8 @@ export const LogsPageContent: React.FunctionComponent = () => { const { initialize } = useLogSourceContext(); + const kibana = useKibana(); + useMount(() => { initialize(); }); @@ -88,6 +90,16 @@ export const LogsPageContent: React.FunctionComponent = () => { + + + {ADD_DATA_LABEL} + + @@ -123,3 +135,7 @@ const settingsTabTitle = i18n.translate('xpack.infra.logs.index.settingsTabTitle }); const feedbackLinkUrl = 'https://discuss.elastic.co/c/logs'; + +const ADD_DATA_LABEL = i18n.translate('xpack.infra.logsHeaderAddDataButtonLabel', { + defaultMessage: 'Add data', +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 35a6cadc786f6..05296fbf6b0a3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; @@ -32,9 +32,15 @@ import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown'; import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { + defaultMessage: 'Add data', +}); + export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; + const kibana = useKibana(); + return ( @@ -102,6 +108,18 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { + + + {ADD_DATA_LABEL} + + diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap index dbdfd6b27e69f..fcf68ad97c8ce 100644 --- a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap @@ -104,6 +104,29 @@ Array [
+
+ +
) : null; + const kibana = useKibana(); + const extraLinkComponents = !extraLinks ? null : ( @@ -64,6 +71,15 @@ export const PageHeader = React.memo( + + + {ADD_DATA_LABEL} + + ); From daf20daf2dc40999e60c18413807bc933bf47d04 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 18 Jun 2020 11:32:13 +0200 Subject: [PATCH 64/68] Beats Management: Pre-migration cleanup (#69155) * adapt KibanaDatabaseAdapter to use core ES service * remove unused `exposeStaticDir` method * create empty KP plugin * remove unused and broken tests * delete unused wallaby config file * delete unused utils * delete unused d.ts and move formsy definition to new plugin * fix findNonExistentItems * remove dead code and useless exports from common package * delete non migratable test suite + remove unused test adapter * remove yet another unused adapter * restore awaits in KibanaDatabaseAdapter --- .../common/config_block_validation.ts | 57 ---- .../common/config_schemas_translations_map.ts | 2 +- .../beats_management/common/domain_types.ts | 2 - .../beats_management/common/io_ts_types.ts | 2 +- .../beats_management/common/return_types.ts | 11 - .../adapters/beats/memory_beats_adapter.ts | 118 --------- .../memory_tags_adapter.ts | 75 ------ .../lib/adapters/database/__tests__/kibana.ts | 40 --- .../database/__tests__/test_contract.ts | 58 ---- .../lib/adapters/database/adapter_types.ts | 11 +- .../database/kibana_database_adapter.ts | 56 ++-- .../lib/adapters/events/adapter_types.ts | 12 - .../elasticsearch_beat_events_adapter.ts | 21 -- .../lib/adapters/framework/adapter_types.ts | 6 +- .../framework/hapi_framework_adapter.ts | 148 ----------- .../framework/integration_tests/kibana.ts | 38 --- .../integration_tests/test_contract.ts | 29 -- .../framework/kibana_framework_adapter.ts | 12 - .../lib/adapters/tags/memory_tags_adapter.ts | 50 ---- .../adapters/tokens/memory_tokens_adapter.ts | 49 ---- .../server/lib/beat_events.ts | 4 +- .../beats_management/server/lib/beats.ts | 10 +- .../server/lib/compose/kibana.ts | 6 +- .../server/lib/compose/testing.ts | 51 ---- .../beats_management/server/lib/framework.ts | 1 - .../__tests__/beats_assignments.test.ts | 249 ------------------ .../server/rest_api/__tests__/data.json | 178 ------------- .../server/rest_api/__tests__/test_harnes.ts | 102 ------- .../beats_management/server/utils/README.md | 1 - .../server/utils/find_non_existent_items.ts | 19 -- .../server/utils/helper_types.ts | 13 - .../utils/index_templates/beats_template.json | 119 --------- .../server/utils/polyfills.ts | 17 -- .../server/utils/wrap_request.ts | 36 --- .../plugins/beats_management/types/eui.d.ts | 16 -- .../plugins/beats_management/wallaby.js | 62 ----- .../plugins/beats_management/server/index.ts | 8 +- .../plugins/beats_management/server/plugin.ts | 39 +++ .../beats_management/types/formsy.d.ts | 0 39 files changed, 83 insertions(+), 1645 deletions(-) delete mode 100644 x-pack/legacy/plugins/beats_management/common/config_block_validation.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json delete mode 100644 x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/README.md delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts delete mode 100644 x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts delete mode 100644 x-pack/legacy/plugins/beats_management/types/eui.d.ts delete mode 100644 x-pack/legacy/plugins/beats_management/wallaby.js create mode 100644 x-pack/plugins/beats_management/server/plugin.ts rename x-pack/{legacy => }/plugins/beats_management/types/formsy.d.ts (100%) diff --git a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts b/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts deleted file mode 100644 index f3d1b9164e976..0000000000000 --- a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import * as t from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { configBlockSchemas } from './config_schemas'; -import { ConfigurationBlock, createConfigurationBlockInterface } from './domain_types'; - -export const validateConfigurationBlocks = (configurationBlocks: ConfigurationBlock[]) => { - const validationMap = { - isHosts: t.array(t.string), - isString: t.string, - isPeriod: t.string, - isPath: t.string, - isPaths: t.array(t.string), - isYaml: t.string, - }; - - for (const [index, block] of configurationBlocks.entries()) { - const blockSchema = configBlockSchemas.find((s) => s.id === block.type); - if (!blockSchema) { - throw new Error( - `Invalid config type of ${block.type} used in 'configuration_blocks' at index ${index}` - ); - } - - const interfaceConfig = blockSchema.configs.reduce((props, config) => { - if (config.options) { - props[config.id] = t.keyof( - Object.fromEntries(config.options.map((opt) => [opt.value, null])) as Record - ); - } else if (config.validation) { - props[config.id] = validationMap[config.validation]; - } - - return props; - }, {} as t.Props); - - const runtimeInterface = createConfigurationBlockInterface( - t.literal(blockSchema.id), - t.interface(interfaceConfig) - ); - - const validationResults = runtimeInterface.decode(block); - - if (isLeft(validationResults)) { - throw new Error( - `configuration_blocks validation error, configuration_blocks at index ${index} is invalid. ${ - PathReporter.report(validationResults)[0] - }` - ); - } - } -}; diff --git a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts index 1aec3e8081708..7cae2a85dc4ca 100644 --- a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts +++ b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ConfigBlockSchema } from './domain_types'; -export const supportedConfigLabelsMap = new Map([ +const supportedConfigLabelsMap = new Map([ [ 'filebeatInputConfig.paths.ui.label', i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsLabel', { diff --git a/x-pack/legacy/plugins/beats_management/common/domain_types.ts b/x-pack/legacy/plugins/beats_management/common/domain_types.ts index b4a9ac8a07479..32e1d81451c65 100644 --- a/x-pack/legacy/plugins/beats_management/common/domain_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/domain_types.ts @@ -7,8 +7,6 @@ import * as t from 'io-ts'; import { configBlockSchemas } from './config_schemas'; import { DateFromString } from './io_ts_types'; -export const OutputTypesArray = ['elasticsearch', 'logstash', 'kafka', 'redis']; - // Here we create the runtime check for a generic, unknown beat config type. // We can also pass in optional params to create spacific runtime checks that // can be used to validate blocs on the API and UI diff --git a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts index d77ad92298699..7d71ea5ad8256 100644 --- a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { isRight } from 'fp-ts/lib/Either'; -export class DateFromStringType extends t.Type { +class DateFromStringType extends t.Type { // eslint-disable-next-line public readonly _tag: 'DateFromISOStringType' = 'DateFromISOStringType'; constructor() { diff --git a/x-pack/legacy/plugins/beats_management/common/return_types.ts b/x-pack/legacy/plugins/beats_management/common/return_types.ts index a7125795a5c7d..7e0e39e12e60a 100644 --- a/x-pack/legacy/plugins/beats_management/common/return_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/return_types.ts @@ -34,11 +34,6 @@ export interface ReturnTypeBulkCreate extends BaseReturnType { }>; } -// delete -export interface ReturnTypeDelete extends BaseReturnType { - action: 'deleted'; -} - export interface ReturnTypeBulkDelete extends BaseReturnType { results: Array<{ success: boolean; @@ -84,12 +79,6 @@ export interface ReturnTypeBulkGet extends BaseReturnType { items: T[]; } -// action -- e.g. validate config block. Like ES simulate endpoint -export interface ReturnTypeAction extends BaseReturnType { - result: { - [key: string]: any; - }; -} // e.g. // { // result: { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts deleted file mode 100644 index afae87c490158..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { intersection, omit } from 'lodash'; - -import { CMBeat } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; - -export class MemoryBeatsAdapter implements CMBeatsAdapter { - private beatsDB: CMBeat[]; - - constructor(beatsDB: CMBeat[]) { - this.beatsDB = beatsDB; - } - - public async get(user: FrameworkUser, id: string) { - return this.beatsDB.find((beat) => beat.id === id) || null; - } - - public async insert(user: FrameworkUser, beat: CMBeat) { - this.beatsDB.push(beat); - } - - public async update(user: FrameworkUser, beat: CMBeat) { - const beatIndex = this.beatsDB.findIndex((b) => b.id === beat.id); - - this.beatsDB[beatIndex] = { - ...this.beatsDB[beatIndex], - ...beat, - }; - } - - public async getWithIds(user: FrameworkUser, beatIds: string[]) { - return this.beatsDB.filter((beat) => beatIds.includes(beat.id)); - } - - public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { - return this.beatsDB.filter((beat) => intersection(tagIds, beat.tags || []).length !== 0); - } - - public async getBeatWithToken( - user: FrameworkUser, - enrollmentToken: string - ): Promise { - return this.beatsDB.find((beat) => enrollmentToken === beat.enrollment_token) || null; - } - - public async getAll(user: FrameworkUser) { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); - } - - public async removeTagsFromBeats( - user: FrameworkUser, - removals: BeatsTagAssignment[] - ): Promise { - const beatIds = removals.map((r) => r.beatId); - - const response = this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - const tagData = removals.find((r) => r.beatId === beat.id); - if (tagData) { - if (beat.tags) { - beat.tags = beat.tags.filter((tag) => tag !== tagData.tag); - } - } - return beat; - }); - - return response.map((item: CMBeat, resultIdx: number) => ({ - idxInRequest: removals[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); - } - - public async assignTagsToBeats( - user: FrameworkUser, - assignments: BeatsTagAssignment[] - ): Promise { - const beatIds = assignments.map((r) => r.beatId); - - this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - // get tags that need to be assigned to this beat - const tags = assignments - .filter((a) => a.beatId === beat.id) - .map((t: BeatsTagAssignment) => t.tag); - - if (tags.length > 0) { - if (!beat.tags) { - beat.tags = []; - } - const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); - - if (nonExistingTags.length > 0) { - beat.tags = beat.tags.concat(nonExistingTags); - } - } - return beat; - }); - - return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); - } - - public setDB(beatsDB: CMBeat[]) { - this.beatsDB = beatsDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts deleted file mode 100644 index ea8a75c92fad2..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Chance from 'chance'; // eslint-disable-line -import { ConfigurationBlock } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { ConfigurationBlockAdapter } from './adapter_types'; - -const chance = new Chance(); - -export class MemoryConfigurationBlockAdapter implements ConfigurationBlockAdapter { - private db: ConfigurationBlock[] = []; - - constructor(db: ConfigurationBlock[]) { - this.db = db.map((config) => { - if (config.id === undefined) { - config.id = chance.word(); - } - return config as ConfigurationBlock & { id: string }; - }); - } - - public async getByIds(user: FrameworkUser, ids: string[]) { - return this.db.filter((block) => ids.includes(block.id)); - } - public async delete(user: FrameworkUser, blockIds: string[]) { - this.db = this.db.filter((block) => !blockIds.includes(block.id)); - return blockIds.map((id) => ({ - id, - success: true, - })); - } - public async deleteForTags( - user: FrameworkUser, - tagIds: string[] - ): Promise<{ success: boolean; reason?: string }> { - this.db = this.db.filter((block) => !tagIds.includes(block.tag)); - return { - success: true, - }; - } - - public async getForTags(user: FrameworkUser, tagIds: string[], page?: number, size?: number) { - const results = this.db.filter((block) => tagIds.includes(block.id)); - return { - page: 0, - total: results.length, - blocks: results, - }; - } - - public async create(user: FrameworkUser, blocks: ConfigurationBlock[]) { - return blocks.map((block) => { - const existingIndex = this.db.findIndex((t) => t.id === block.id); - if (existingIndex !== -1) { - this.db[existingIndex] = block; - } else { - this.db.push(block); - } - return block.id; - }); - } - - public setDB(db: ConfigurationBlock[]) { - this.db = db.map((block) => { - if (block.id === undefined) { - block.id = chance.word(); - } - return block; - }); - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts deleted file mode 100644 index 460fc412e9491..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -// file.skip - -// @ts-ignore -import { createLegacyEsTestCluster } from '@kbn/test'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from 'src/core/server/root'; -// @ts-ignore -import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server'; -import { DatabaseKbnESPlugin } from '../adapter_types'; -import { KibanaDatabaseAdapter } from '../kibana_database_adapter'; -import { contractTests } from './test_contract'; -const es = createLegacyEsTestCluster({}); - -let legacyServer: any; -let rootServer: Root; -contractTests('Kibana Database Adapter', { - before: async () => { - await es.start(); - - rootServer = kbnTestServer.createRootWithCorePlugins({ - server: { maxPayloadBytes: 100 }, - }); - - await rootServer.setup(); - legacyServer = kbnTestServer.getKbnServer(rootServer); - return await legacyServer.plugins.elasticsearch.waitUntilReady(); - }, - after: async () => { - await rootServer.shutdown(); - return await es.cleanup(); - }, - adapterSetup: () => { - return new KibanaDatabaseAdapter(legacyServer.plugins.elasticsearch as DatabaseKbnESPlugin); - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts deleted file mode 100644 index 369c2e1056211..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { DatabaseAdapter } from '../adapter_types'; - -interface ContractConfig { - before?(): Promise; - after?(): Promise; - adapterSetup(): DatabaseAdapter; -} - -export const contractTests = (testName: string, config: ContractConfig) => { - describe.skip(testName, () => { - let database: DatabaseAdapter; - beforeAll(async () => { - jest.setTimeout(100000); // 1 second - - if (config.before) { - await config.before(); - } - }); - afterAll(async () => config.after && (await config.after())); - beforeEach(async () => { - database = config.adapterSetup(); - }); - - it('Unauthorized users cant query', async () => { - const params = { - id: `beat:foo`, - ignore: [404], - index: '.management-beats', - }; - let ranWithoutError = false; - try { - await database.get({ kind: 'unauthenticated' }, params); - ranWithoutError = true; - } catch (e) { - expect(e).not.toEqual(null); - } - expect(ranWithoutError).toEqual(false); - }); - - it('Should query ES', async () => { - const params = { - id: `beat:foo`, - ignore: [404], - index: '.management-beats', - }; - const response = await database.get({ kind: 'internal' }, params); - - expect(response).not.toEqual(undefined); - // @ts-ignore - expect(response.found).toEqual(undefined); - }); - }); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts index 0a06c3dcc6412..90519840af213 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FrameworkRequest, FrameworkUser } from '../framework/adapter_types'; +import { FrameworkUser } from '../framework/adapter_types'; export interface DatabaseAdapter { get( @@ -39,15 +39,6 @@ export interface DatabaseAdapter { putTemplate(name: string, template: any): Promise; } -export interface DatabaseKbnESCluster { - callWithInternalUser(esMethod: string, options: {}): Promise; - callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise; -} - -export interface DatabaseKbnESPlugin { - getCluster(clusterName: string): DatabaseKbnESCluster; -} - export interface DatabaseSearchParams extends DatabaseGenericParams { analyzer?: string; analyzeWildcard?: boolean; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts index 1ca3bcae8bfca..baccbe416f398 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceStart, IClusterClient } from 'src/core/server'; import { FrameworkUser } from '../framework/adapter_types'; import { internalAuthData } from './../framework/adapter_types'; import { @@ -15,8 +16,6 @@ import { DatabaseGetDocumentResponse, DatabaseGetParams, DatabaseIndexDocumentParams, - DatabaseKbnESCluster, - DatabaseKbnESPlugin, DatabaseMGetParams, DatabaseMGetResponse, DatabaseSearchParams, @@ -24,75 +23,67 @@ import { } from './adapter_types'; export class KibanaDatabaseAdapter implements DatabaseAdapter { - private es: DatabaseKbnESCluster; + private es: IClusterClient; - constructor(kbnElasticSearch: DatabaseKbnESPlugin) { - this.es = kbnElasticSearch.getCluster('admin'); + constructor(elasticsearch: ElasticsearchServiceStart) { + this.es = elasticsearch.legacy.client; } public async get( user: FrameworkUser, params: DatabaseGetParams ): Promise> { - const result = await this.callWithUser(user, 'get', params); - return result; - // todo + return await this.callWithUser(user, 'get', params); } public async mget( user: FrameworkUser, params: DatabaseMGetParams ): Promise> { - const result = await this.callWithUser(user, 'mget', params); - return result; - // todo + return await this.callWithUser(user, 'mget', params); } public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { - const result = await this.callWithUser(user, 'bulk', params); - return result; + return await this.callWithUser(user, 'bulk', params); } public async create( user: FrameworkUser, params: DatabaseCreateDocumentParams ): Promise { - const result = await this.callWithUser(user, 'create', params); - return result; + return await this.callWithUser(user, 'create', params); } + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { - const result = await this.callWithUser(user, 'index', params); - return result; + return await this.callWithUser(user, 'index', params); } + public async delete( user: FrameworkUser, params: DatabaseDeleteDocumentParams ): Promise { - const result = await this.callWithUser(user, 'delete', params); - return result; + return await this.callWithUser(user, 'delete', params); } public async deleteByQuery( user: FrameworkUser, params: DatabaseSearchParams ): Promise { - const result = await this.callWithUser(user, 'deleteByQuery', params); - return result; + return await this.callWithUser(user, 'deleteByQuery', params); } public async search( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', params); - return result; + return await this.callWithUser(user, 'search', params); } public async searchAll( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', { + return await this.callWithUser(user, 'search', { scroll: '1m', ...params, body: { @@ -100,29 +91,24 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { ...params.body, }, }); - return result; } public async putTemplate(name: string, template: any): Promise { - const result = await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { + return await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { name, body: template, }); - - return result; } private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any { if (user.kind === 'authenticated') { - return this.es.callWithRequest( - { + return this.es + .asScoped({ headers: user[internalAuthData], - } as any, - esMethod, - options - ); + }) + .callAsCurrentUser(esMethod, options); } else if (user.kind === 'internal') { - return this.es.callWithInternalUser(esMethod, options); + return this.es.callAsInternalUser(esMethod, options); } else { throw new Error('Invalid user type'); } diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts deleted file mode 100644 index 4cb38bb3d057b..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; - -export interface BeatEventsAdapter { - bulkInsert(user: FrameworkUser, beatId: string, events: BeatEvent[]): Promise; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts deleted file mode 100644 index b5056140c8b86..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { BeatEventsAdapter } from './adapter_types'; - -export class ElasticsearchBeatEventsAdapter implements BeatEventsAdapter { - // @ts-ignore - constructor(private readonly database: DatabaseAdapter) {} - - // eslint-disable-next-line - public bulkInsert = async (user: FrameworkUser, beatId: string, events: BeatEvent[]) => { - // await this.database.putTemplate(INDEX_NAMES.EVENTS_TODAY, beatsIndexTemplate); - }; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 80599f38d982a..e2703cb5786dd 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,7 @@ import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; import { LicenseType } from '../../../../common/constants/security'; @@ -33,7 +34,6 @@ export interface BackendFrameworkAdapter { log(text: string): void; on(event: 'xpack.status.green' | 'elasticsearch.status.green', cb: () => void): void; getSetting(settingPath: string): any; - exposeStaticDir(urlPath: string, dir: string): void; registerRoute( route: FrameworkRouteOptions ): void; @@ -42,8 +42,12 @@ export interface BackendFrameworkAdapter { export interface KibanaLegacyServer { newPlatform: { setup: { + core: CoreSetup; plugins: { security: SecurityPluginSetup }; }; + start: { + core: CoreStart; + }; }; plugins: { xpack_main: { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts deleted file mode 100644 index 90500e0283511..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LicenseType } from './../../../../common/constants/security'; -import { KibanaServerRequest } from './adapter_types'; -import { - BackendFrameworkAdapter, - FrameworkInfo, - FrameworkRequest, - FrameworkResponse, - FrameworkRouteOptions, - internalAuthData, - internalUser, -} from './adapter_types'; - -interface TestSettings { - enrollmentTokensTtlInSeconds: number; - encryptionKey: string; -} - -export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter { - public info: null | FrameworkInfo = null; - public readonly internalUser = internalUser; - - private settings: TestSettings; - private server: any; - - constructor( - settings: TestSettings = { - encryptionKey: 'something_who_cares', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - }, - hapiServer?: any, - license: LicenseType = 'trial', - securityEnabled: boolean = true, - licenseActive: boolean = true - ) { - this.server = hapiServer; - this.settings = settings; - const now = new Date(); - - this.info = { - kibana: { - version: 'unknown', - }, - license: { - type: license, - expired: !licenseActive, - expiry_date_in_millis: new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime(), - }, - security: { - enabled: securityEnabled, - available: securityEnabled, - }, - watcher: { - enabled: true, - available: true, - }, - }; - } - public log(text: string) { - this.server.log(text); - } - public on(event: 'xpack.status.green', cb: () => void) { - cb(); - } - public getSetting(settingPath: string) { - switch (settingPath) { - case 'xpack.beats.enrollmentTokensTtlInSeconds': - return this.settings.enrollmentTokensTtlInSeconds; - case 'xpack.beats.encryptionKey': - return this.settings.encryptionKey; - } - } - - public exposeStaticDir(urlPath: string, dir: string): void { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use exposeStaticDir'); - } - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - - public registerRoute< - RouteRequest extends FrameworkRequest, - RouteResponse extends FrameworkResponse - >(route: FrameworkRouteOptions) { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use registerRoute'); - } - const wrappedHandler = (licenseRequired: string[]) => (request: any, h: any) => { - return route.handler(this.wrapRequest(request), h); - }; - - this.server.route({ - handler: wrappedHandler(route.licenseRequired || []), - method: route.method, - path: route.path, - config: { - ...route.config, - auth: false, - }, - }); - } - - public async injectRequstForTesting({ method, url, headers, payload }: any) { - return await this.server.inject({ method, url, headers, payload }); - } - - private wrapRequest( - req: InternalRequest - ): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - username: 'elastic', - roles: ['superuser'], - full_name: null, - email: null, - enabled: true, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts deleted file mode 100644 index 4f0ba01b86082..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -// file.skip - -import { camelCase } from 'lodash'; -// @ts-ignore -import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server'; -// @ts-ignore -import { TestKbnServerConfig } from '../../../../../../../test_utils/kbn_server_config'; -import { CONFIG_PREFIX } from '../../../../../common/constants/plugin'; -import { PLUGIN } from './../../../../../common/constants/plugin'; -import { KibanaBackendFrameworkAdapter } from './../kibana_framework_adapter'; -import { contractTests } from './test_contract'; - -let kbnServer: any; -let kbn: any; -let esServer: any; -contractTests('Kibana Framework Adapter', { - async before() { - const servers = kbnTestServer.createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: TestKbnServerConfig, - }); - esServer = await servers.startES(); - kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; - }, - async after() { - await kbn.stop(); - await esServer.stop(); - }, - adapterSetup: () => { - return new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), kbnServer.server, CONFIG_PREFIX); - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts deleted file mode 100644 index 8e21f8cf78ad7..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.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; - * you may not use this file except in compliance with the Elastic License. - */ -import { BackendFrameworkAdapter } from '../adapter_types'; - -interface ContractConfig { - before(): Promise; - after(): Promise; - adapterSetup(): BackendFrameworkAdapter; -} - -export const contractTests = (testName: string, config: ContractConfig) => { - describe(testName, () => { - let frameworkAdapter: BackendFrameworkAdapter; - beforeAll(config.before); - afterAll(config.after); - beforeEach(async () => { - frameworkAdapter = config.adapterSetup(); - }); - - it('Should have tests here', () => { - expect(frameworkAdapter.info).toHaveProperty('server'); - - expect(frameworkAdapter).toHaveProperty('server'); - }); - }); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 1bf9bbb22b352..3b29e50e4465b 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -68,18 +68,6 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { this.server.log(text); } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - public registerRoute< RouteRequest extends FrameworkRequest, RouteResponse extends FrameworkResponse diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts deleted file mode 100644 index 66a6c7ebebc2c..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BeatTag } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class MemoryTagsAdapter implements CMTagsAdapter { - private tagsDB: BeatTag[] = []; - - constructor(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } - - public async getAll(user: FrameworkUser) { - return this.tagsDB; - } - public async delete(user: FrameworkUser, tagIds: string[]) { - this.tagsDB = this.tagsDB.filter((tag) => !tagIds.includes(tag.id)); - - return true; - } - public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { - return this.tagsDB.filter((tag) => tagIds.includes(tag.id)); - } - - public async upsertTag(user: FrameworkUser, tag: BeatTag) { - const existingTagIndex = this.tagsDB.findIndex((t) => t.id === tag.id); - if (existingTagIndex !== -1) { - this.tagsDB[existingTagIndex] = tag; - } else { - this.tagsDB.push(tag); - } - return tag.id; - } - - public async getWithoutConfigTypes( - user: FrameworkUser, - blockTypes: string[] - ): Promise { - return this.tagsDB.filter((tag) => tag.hasConfigurationBlocksTypes.includes(blockTypes[0])); - } - - public setDB(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts deleted file mode 100644 index 431263c808b45..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkAuthenticatedUser, FrameworkUser } from '../framework/adapter_types'; -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; - -export class MemoryTokensAdapter implements CMTokensAdapter { - private tokenDB: TokenEnrollmentData[]; - - constructor(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } - - public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - const index = this.tokenDB.findIndex((token) => token.token === enrollmentToken); - - if (index > -1) { - this.tokenDB.splice(index, 1); - } - } - - public async getEnrollmentToken( - user: FrameworkUser, - tokenString: string - ): Promise { - return new Promise((resolve) => { - return resolve(this.tokenDB.find((token) => token.token === tokenString)); - }); - } - - public async insertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) { - tokens.forEach((token) => { - const existingIndex = this.tokenDB.findIndex((t) => t.token === token.token); - if (existingIndex !== -1) { - this.tokenDB[existingIndex] = token; - } else { - this.tokenDB.push(token); - } - }); - return tokens; - } - - public setDB(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts index f4cb3cb424f6f..54782783f94ca 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts @@ -6,13 +6,11 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { BeatEvent, RuntimeBeatEvent } from '../../common/domain_types'; -import { BeatEventsAdapter } from './adapters/events/adapter_types'; import { FrameworkUser } from './adapters/framework/adapter_types'; import { CMBeatsDomain } from './beats'; export class BeatEventsLib { - // @ts-ignore - constructor(private readonly adapter: BeatEventsAdapter, private readonly beats: CMBeatsDomain) {} + constructor(private readonly beats: CMBeatsDomain) {} public log = async ( user: FrameworkUser, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts index 3b9c4d35d8331..6b7053f40550b 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts @@ -7,7 +7,6 @@ import { uniq } from 'lodash'; import moment from 'moment'; import { CMBeat } from '../../common/domain_types'; -import { findNonExistentItems } from '../utils/find_non_existent_items'; import { BeatsRemovalReturn, BeatsTagAssignment, @@ -249,3 +248,12 @@ function addToResultsToResponse(key: string, response: any, assignmentResults: a }); return response; } + +export function findNonExistentItems(items: Array<{ id: string }>, requestedItems: string[]) { + return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { + if (items.findIndex((item) => item && item.id === requestedItem) === -1) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); +} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts index 2bda2fe85d62f..b6a645ded8164 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts @@ -9,9 +9,7 @@ import { PLUGIN } from '../../../common/constants'; import { CONFIG_PREFIX } from '../../../common/constants/plugin'; import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; import { ElasticsearchConfigurationBlockAdapter } from '../adapters/configuration_blocks/elasticsearch_configuration_block_adapter'; -import { DatabaseKbnESPlugin } from '../adapters/database/adapter_types'; import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; -import { ElasticsearchBeatEventsAdapter } from '../adapters/events/elasticsearch_beat_events_adapter'; import { KibanaLegacyServer } from '../adapters/framework/adapter_types'; import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; @@ -28,7 +26,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { const framework = new BackendFrameworkLib( new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), server, CONFIG_PREFIX) ); - const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch as DatabaseKbnESPlugin); + const database = new KibanaDatabaseAdapter(server.newPlatform.start.core.elasticsearch); const beatsAdapter = new ElasticsearchBeatsAdapter(database); const configAdapter = new ElasticsearchConfigurationBlockAdapter(database); @@ -46,7 +44,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { tokens, framework, }); - const beatEvents = new BeatEventsLib(new ElasticsearchBeatEventsAdapter(database), beats); + const beatEvents = new BeatEventsLib(beats); const libs: CMServerLibs = { beatEvents, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts deleted file mode 100644 index b5fe6195fc7c7..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; -import { MemoryConfigurationBlockAdapter } from '../adapters/configuration_blocks/memory_tags_adapter'; -import { HapiBackendFrameworkAdapter } from '../adapters/framework/hapi_framework_adapter'; -import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; -import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; -import { BeatEventsLib } from '../beat_events'; -import { CMBeatsDomain } from '../beats'; -import { ConfigurationBlocksLib } from '../configuration_blocks'; -import { BackendFrameworkLib } from '../framework'; -import { CMTagsDomain } from '../tags'; -import { CMTokensDomain } from '../tokens'; -import { CMServerLibs } from '../types'; - -export function compose(server: any): CMServerLibs { - const framework = new BackendFrameworkLib(new HapiBackendFrameworkAdapter(undefined, server)); - - const beatsAdapter = new MemoryBeatsAdapter(server.beatsDB || []); - const configAdapter = new MemoryConfigurationBlockAdapter(server.configsDB || []); - const tags = new CMTagsDomain( - new MemoryTagsAdapter(server.tagsDB || []), - configAdapter, - beatsAdapter - ); - const configurationBlocks = new ConfigurationBlocksLib(configAdapter, tags); - const tokens = new CMTokensDomain(new MemoryTokensAdapter(server.tokensDB || []), { - framework, - }); - const beats = new CMBeatsDomain(beatsAdapter, { - tags, - tokens, - framework, - }); - const beatEvents = new BeatEventsLib({} as any, beats); - - const libs: CMServerLibs = { - beatEvents, - framework, - beats, - tags, - tokens, - configurationBlocks, - }; - - return libs; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts index 1a6f84a6979c6..96a06929073e5 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts @@ -16,7 +16,6 @@ import { export class BackendFrameworkLib { public log = this.adapter.log; public on = this.adapter.on.bind(this.adapter); - public exposeStaticDir = this.adapter.exposeStaticDir; public internalUser = this.adapter.internalUser; constructor(private readonly adapter: BackendFrameworkAdapter) { this.validateConfig(); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts deleted file mode 100644 index 156304443431d..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CMServerLibs } from '../../lib/types'; -import { HapiBackendFrameworkAdapter } from './../../lib/adapters/framework/hapi_framework_adapter'; -import { testHarnes } from './test_harnes'; - -describe('assign_tags_to_beats', () => { - let serverLibs: CMServerLibs; - - beforeAll(async () => { - jest.setTimeout(100000); // 1 second - - serverLibs = await testHarnes.getServerLibs(); - }); - beforeEach(async () => await testHarnes.loadData()); - - it('should add a single tag to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'bar', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - }); - - it('should not re-add an existing tag to a beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'foo', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa']); // as - }); - - it('should add a single tag to a multiple beats', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'development' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let beat; - - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development']); - }); - - it('should add multiple tags to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'bar', tag: 'development' }, - { beatId: 'bar', tag: 'production' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development', 'production']); - }); - - // it('should add multiple tags to a multiple beats', async () => { - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 200, result: 'updated' }, - // { status: 200, result: 'updated' }, - // ]); - - // let esResponse; - // let beat; - - // // Beat foo - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:foo`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // // Beat bar - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production']); - // }); - - // it('should return errors for non-existent beats', async () => { - // const nonExistentBeatId = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} not found` }, - // ]); - // }); - - // it('should return errors for non-existent tags', async () => { - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'bar', tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); - - // it('should return errors for non-existent beats and tags', async () => { - // const nonExistentBeatId = chance.word(); - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatID: nonExistentBeatId, tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json deleted file mode 100644 index 4ee5a4a7e2d55..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:qux", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "qux", - "name": "qux_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:baz", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "22.33.11.44", - "host_name": "baz.bar.com", - "id": "baz", - "name": "baz_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:foo", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "foo", - "name": "foo_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", - "verified_on": "2018-05-15T16:25:38.924Z", - "tags": [ - "production", - "qa" - ] - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:bar", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "11.22.33.44", - "host_name": "foo.com", - "id": "bar", - "name": "bar_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:production", - "source": { - "type": "tag", - "tag": { - "color": "blue" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:development", - "source": { - "type": "tag", - "tag": { - "color": "red" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:qa", - "source": { - "type": "tag", - "tag": { - "color": "green" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:SDfsdfIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "output", - "description": "some description", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"username\": \"some-username\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"module\": \"memcached\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdwcYyG50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "qa", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{\"module\": \"memcached\", \"node.namespace\": \"node\", \"hosts\": [\"localhost:4949\"] }" - } - } - } -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts deleted file mode 100644 index 590ce0bd7b287..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { badRequest } from 'boom'; -import { readFile } from 'fs'; -// @ts-ignore -import Hapi from 'hapi'; -import { resolve } from 'path'; -import { promisify } from 'util'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { TokenEnrollmentData } from '../../lib/adapters/tokens/adapter_types'; -import { compose } from '../../lib/compose/testing'; -import { CMServerLibs } from '../../lib/types'; -import { initManagementServer } from './../../management_server'; - -const readFileAsync = promisify(readFile); -let serverLibs: CMServerLibs; - -export const testHarnes = { - description: 'API Development Tests', - loadData: async () => { - if (!serverLibs) { - throw new Error('Server libs not composed yet...'); - } - const contents = await readFileAsync(resolve(__dirname, './data.json'), 'utf8'); - const database = contents.split(/\n\n/); - - // @ts-ignore the private access - serverLibs.beats.adapter.setDB( - database.reduce((inserts: CMBeat[], source) => { - const type = 'beat'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tags.adapter.setDB( - database.reduce((inserts: BeatTag[], source) => { - const type = 'tag'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tokens.adapter.setDB( - database.reduce((inserts: TokenEnrollmentData[], source) => { - const type = 'token'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - }, - getServerLibs: async () => { - if (!serverLibs) { - const server = new Hapi.Server({ port: 111111 }); - const versionHeader = 'kbn-version'; - const xsrfHeader = 'kbn-xsrf'; - - server.ext('onPostAuth', (req: any, h: any) => { - const isSafeMethod = req.method === 'get' || req.method === 'head'; - const hasVersionHeader = versionHeader in req.headers; - const hasXsrfHeader = xsrfHeader in req.headers; - - if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { - throw badRequest(`Request must contain a ${xsrfHeader} header.`); - } - - return h.continue; - }); - - serverLibs = compose(server); - initManagementServer(serverLibs); - } - return serverLibs; - }, -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/README.md b/x-pack/legacy/plugins/beats_management/server/utils/README.md deleted file mode 100644 index 8a6a27aa29867..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/README.md +++ /dev/null @@ -1 +0,0 @@ -Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts b/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts deleted file mode 100644 index 0e9b4f0b6fa5e..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -interface RandomItem { - id: string; - [key: string]: any; -} - -export function findNonExistentItems(items: RandomItem[], requestedItems: any) { - return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { - if (items.findIndex((item: RandomItem) => item && item.id === requestedItem) === -1) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts b/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts deleted file mode 100644 index 96f7b7bc79b62..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type InterfaceExcept = Pick>; - -export function arrayFromEnum(e: any): T[] { - return Object.keys(e) - .filter((key) => isNaN(+key)) - .map((name) => e[name]) as T[]; -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json deleted file mode 100644 index ba3a0aba6c256..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "index_patterns": [".management-beats"], - "version": 66000, - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-1", - "codec": "best_compression" - } - }, - "mappings": { - "_doc": { - "dynamic": "strict", - "properties": { - "type": { - "type": "keyword" - }, - "configuration_block": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "tag": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "config": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - } - } - }, - "enrollment_token": { - "properties": { - "token": { - "type": "keyword" - }, - "expires_on": { - "type": "date" - } - } - }, - "tag": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "hasConfigurationBlocksTypes": { - "type": "keyword" - } - } - }, - "beat": { - "properties": { - "id": { - "type": "keyword" - }, - "config_status": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "last_checkin": { - "type": "date" - }, - "enrollment_token": { - "type": "keyword" - }, - "access_token": { - "type": "keyword" - }, - "verified_on": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "host_ip": { - "type": "ip" - }, - "host_name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "metadata": { - "dynamic": "true", - "type": "object" - }, - "name": { - "type": "keyword" - } - } - } - } - } - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts b/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts deleted file mode 100644 index 5291e2c72be7d..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const entries = (obj: any) => { - const ownProps = Object.keys(obj); - let i = ownProps.length; - const resArray = new Array(i); // preallocate the Array - - while (i--) { - resArray[i] = [ownProps[i], obj[ownProps[i]]]; - } - - return resArray; -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts b/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts deleted file mode 100644 index 57cf70a99a296..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - FrameworkRequest, - internalAuthData, - KibanaServerRequest, -} from '../lib/adapters/framework/adapter_types'; - -export function wrapRequest( - req: InternalRequest -): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - // @ts-ignore -- partial applucation, adapter adds other user data - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/beats_management/types/eui.d.ts b/x-pack/legacy/plugins/beats_management/types/eui.d.ts deleted file mode 100644 index 636d0a2f7b51e..0000000000000 --- a/x-pack/legacy/plugins/beats_management/types/eui.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * /!\ These type definitions are temporary until the upstream @elastic/eui - * package includes them. - */ - -import * as eui from '@elastic/eui'; -import { Moment } from 'moment'; -import { ChangeEventHandler, MouseEventHandler, ReactType, Ref, FC } from 'react'; - -declare module '@elastic/eui' {} diff --git a/x-pack/legacy/plugins/beats_management/wallaby.js b/x-pack/legacy/plugins/beats_management/wallaby.js deleted file mode 100644 index 823f63b15bcb3..0000000000000 --- a/x-pack/legacy/plugins/beats_management/wallaby.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -const path = require('path'); -process.env.NODE_PATH = path.resolve(__dirname, '..', '..', '..', 'node_modules'); - -module.exports = function (wallaby) { - return { - debug: true, - files: [ - './tsconfig.json', - //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - '!**/*.test.ts', - ], - - tests: ['**/*.test.ts', '**/*.test.tsx'], - env: { - type: 'node', - runner: 'node', - }, - testFramework: { - type: 'jest', - //path: jestPath, - }, - compilers: { - '**/*.ts?(x)': wallaby.compilers.typeScript({ - typescript: require('typescript'), // eslint-disable-line - }), - '**/*.js': wallaby.compilers.babel({ - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/node_preset')], - }), - }, - - setup: (wallaby) => { - const path = require('path'); - - const kibanaDirectory = path.resolve(wallaby.localProjectDir, '..', '..', '..'); - wallaby.testFramework.configure({ - rootDir: wallaby.localProjectDir, - moduleNameMapper: { - '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, - // eslint-disable-next-line - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, - '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, - }, - testURL: 'http://localhost', - setupFiles: [`${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`], - snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], - transform: { - '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, - //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, - }, - }); - }, - }; -}; diff --git a/x-pack/plugins/beats_management/server/index.ts b/x-pack/plugins/beats_management/server/index.ts index 607fb0ab2725d..ad19087f5ac9f 100644 --- a/x-pack/plugins/beats_management/server/index.ts +++ b/x-pack/plugins/beats_management/server/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializer } from '../../../../src/core/server'; import { beatsManagementConfigSchema } from '../common'; +import { BeatsManagementPlugin } from './plugin'; export const config = { schema: beatsManagementConfigSchema, @@ -16,8 +18,4 @@ export const config = { }, }; -export const plugin = () => ({ - setup() {}, - start() {}, - stop() {}, -}); +export const plugin: PluginInitializer<{}, {}> = (context) => new BeatsManagementPlugin(context); diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts new file mode 100644 index 0000000000000..a82dbcb4a3a6e --- /dev/null +++ b/x-pack/plugins/beats_management/server/plugin.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../../src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { LicensingPluginStart } from '../../licensing/server'; +import { BeatsManagementConfigType } from '../common'; + +interface SetupDeps { + security?: SecurityPluginSetup; +} + +interface StartDeps { + licensing: LicensingPluginStart; +} + +export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + constructor( + private readonly initializerContext: PluginInitializerContext + ) {} + + public async setup(core: CoreSetup, plugins: SetupDeps) { + this.initializerContext.config.create(); + + return {}; + } + + public async start(core: CoreStart, { licensing }: StartDeps) { + return {}; + } +} diff --git a/x-pack/legacy/plugins/beats_management/types/formsy.d.ts b/x-pack/plugins/beats_management/types/formsy.d.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/types/formsy.d.ts rename to x-pack/plugins/beats_management/types/formsy.d.ts From 7dd4fa2618accbb26f56ddb2b18aade7b5d1f6df Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 18 Jun 2020 11:37:41 +0200 Subject: [PATCH 65/68] Add section about marble testing to `TESTING.md` (#68749) * Add section about marble testing * improve `callServerAPI` example * review comments * add comment on abort observable anti-pattern --- src/core/TESTING.md | 276 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/src/core/TESTING.md b/src/core/TESTING.md index bed41ab583496..a62922d9b5d64 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -29,6 +29,14 @@ This document outlines best practices and patterns for testing Kibana Plugins. - [Testing dependencies usages](#testing-dependencies-usages) - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) + - [RXJS testing](#rxjs-testing) + - [Testing RXJS observables with marble](#rxjs-testing-with-marble) + - [Precondition](#preconditions-2) + - [Examples](#example-5) + - [Testing an interval based observable](#testing-an-interval-based-observable) + - [Testing observable completion](#testing-observable-completion) + - [Testing observable errors](#testing-observable-errors) + - [Testing promise based observables](#testing-promise-based-observables) ## Strategy @@ -1087,3 +1095,271 @@ describe('Plugin', () => { }); }); ``` + +## RXJS testing + +### Testing RXJS observables with marble + +Testing observable based APIs can be challenging, specially when asynchronous operators or sources are used, +or when trying to assert against emission's timing. + +Fortunately, RXJS comes with it's own `marble` testing module to greatly facilitate that kind of testing. + +See [the official doc](https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing) for more information about marble testing. + +### Preconditions + +The following examples all assume that the following snippet is included in every test file: + +```typescript +import { TestScheduler } from 'rxjs/testing'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); +``` + +`getTestScheduler` creates a `TestScheduler` that is wired on `jest`'s `expect` statement when comparing an observable's time frame. + +### Examples + +#### Testing an interval based observable + +Here is a very basic example of an interval-based API: + +```typescript +class FooService { + setup() { + return { + getUpdate$: () => { + return interval(100).pipe(map((count) => `update-${count + 1}`)); + }, + }; + } +} +``` + +If we were to be adding a test that asserts the correct behavior of this API without using marble testing, it +would probably be something like: + +```typescript +it('getUpdate$ emits updates every 100ms', async () => { + const service = new FooService(); + const { getUpdate$ } = service.setup(); + expect(await getUpdate$().pipe(take(3), toArray()).toPromise()).toEqual([ + 'update-1', + 'update-2', + 'update-3', + ]); +}); +``` + +Note that if we are able to test the correct value of each emission, we don't have any way to assert that +the interval of 100ms was respected. Even using a subscription based test to try to do so would result +in potential flakiness, as the subscription execution could trigger on the `101ms` time frame for example. + +It also may be important to note: +- as we need to convert the observable to a promise and wait for the result, the test is `async` +- we need to perform observable transformation (`take` + `toArray`) in the test to have an usable value to assert against. + +Marble testing would allow to get rid of these limitations. An equivalent and improved marble test could be: + +```typescript + describe('getUpdate$', () => { + it('emits updates every 100ms', () => { + getTestScheduler().run(({ expectObservable }) => { + const { getUpdate$ } = service.setup(); + expectObservable(getUpdate$(), '301ms !').toBe('100ms a 99ms b 99ms c', { + a: 'update-1', + b: 'update-2', + c: 'update-3', + }); + }); + }); + }); +``` + +Notes: +- the test is now synchronous +- the second parameter of `expectObservable` (`'301ms !'`) is used to perform manual unsubscription to the observable, as + `interval` never ends. +- an emission is considered a time frame, meaning that after the initial `a` emission, we are at the frame `101`, not `100` + which is why we are then only using a `99ms` gap between a->b and b->c. + +#### Testing observable completion + +Let's 'improve' our `getUpdate$` API by allowing the consumer to manually terminate the observable chain using +a new `abort$` option: + +```typescript +class FooService { + setup() { + return { + // note: using an abortion observable is usually an anti-pattern, as unsubscribing from the observable + // is, most of the time, a better solution. This is only used for the example purpose. + getUpdate$: ({ abort$ = EMPTY }: { abort$?: Observable } = {}) => { + return interval(100).pipe( + takeUntil(abort$), + map((count) => `update-${count + 1}`) + ); + }, + }; + } +} +``` + +We would then add a test to assert than this new option usage is respected: + +```typescript +it('getUpdate$ completes when `abort$` emits', () => { + const service = new FooService(); + getTestScheduler().run(({ expectObservable, hot }) => { + const { getUpdate$ } = service.setup(); + const abort$ = hot('149ms a', { a: undefined }); + expectObservable(getUpdate$({ abort$ })).toBe('100ms a 48ms |', { + a: 'update-1', + }); + }); +}); +``` + +Notes: + - the `|` symbol represents the completion of the observable. + - we are here using the `hot` testing utility to create the `abort$` observable to ensure correct emission timing. + +#### Testing observable errors + +Testing errors thrown by the observable is very close to the previous examples and is done using +the third parameter of `expectObservable`. + +Say we have a service in charge of processing data from an observable and returning the results in a new observable: + +```typescript +interface SomeDataType { + id: string; +} + +class BarService { + setup() { + return { + processDataStream: (data$: Observable) => { + return data$.pipe( + map((data) => { + if (data.id === 'invalid') { + throw new Error(`invalid data: '${data.id}'`); + } + return { + ...data, + processed: 'additional-data', + }; + }) + ); + }, + }; + } +} +``` + +We could write a test that asserts the service properly emit processed results until an invalid data is encountered: + +```typescript +it('processDataStream throw an error when processing invalid data', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const service = new BarService(); + const { processDataStream } = service.setup(); + + const data = hot('--a--b--(c|)', { + a: { id: 'a' }, + b: { id: 'invalid' }, + c: { id: 'c' }, + }); + + expectObservable(processDataStream(data)).toBe( + '--a--#', + { + a: { id: 'a', processed: 'additional-data' }, + }, + `'[Error: invalid data: 'invalid']'` + ); + }); +}); +``` + +Notes: + - the `-` symbol represents one virtual time frame. + - the `#` symbol represents an error. + - when throwing custom `Error` classes, the assertion can be against an error instance, but this doesn't work + with base errors. + +#### Testing promise based observables + +In some cases, the observable we want to test is based on a Promise (like `of(somePromise).pipe(...)`). This can occur +when using promise-based services, such as core's `http`, for instance. + +```typescript +export const callServerAPI = ( + http: HttpStart, + body: Record, + { abort$ }: { abort$: Observable } +): Observable => { + let controller: AbortController | undefined; + if (abort$) { + controller = new AbortController(); + abort$.subscribe(() => { + controller!.abort(); + }); + } + return from( + http.post('/api/endpoint', { + body, + signal: controller?.signal, + }) + ).pipe( + takeUntil(abort$ ?? EMPTY), + map((response) => response.results) + ); +}; +``` + +Testing that kind of promise based observable does not work out of the box with marble testing, as the asynchronous promise resolution +is not handled by the test scheduler's 'sandbox'. + +Fortunately, there are workarounds for this problem. The most common one being to mock the promise-returning API to return +an observable instead for testing, as `of(observable)` also works and returns the input observable. + +Note that when doing so, the test suite must also include tests using a real promise value to ensure correct behavior in real situation. + +```typescript + +// NOTE: test scheduler do not properly work with promises because of their asynchronous nature. +// we are cheating here by having `http.post` return an observable instead of a promise. +// this still allows more finely grained testing about timing, and asserting that the method +// works properly when `post` returns a real promise is handled in other tests of this suite + +it('callServerAPI result observable emits when the response is received', () => { + const http = httpServiceMock.createStartContract(); + getTestScheduler().run(({ expectObservable, hot }) => { + // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment + http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any); + + const results = callServerAPI(http, { query: 'term' }, {}); + + expectObservable(results).toBe('---(a|)', { + a: { someData: 'foo' }, + }); + }); +}); + +it('completes without returning results if aborted$ emits before the response', () => { + const http = httpServiceMock.createStartContract(); + getTestScheduler().run(({ expectObservable, hot }) => { + // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment + http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any); + const aborted$ = hot('-(a|)', { a: undefined }); + const results = callServerAPI(http, { query: 'term' }, { aborted$ }); + + expectObservable(results).toBe('-|'); + }); +}); +``` \ No newline at end of file From d2006ea8a050448ec3d835a9b0c7a56763379c2c Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 18 Jun 2020 11:39:25 +0200 Subject: [PATCH 66/68] savedObjects: add `score` to repository.find results (#68894) * add `score` to repository.find results * update generated doc * fix FTR result set * remove score from exports * fix FTR for find API * fix label * fix tsdoc --- .../core/server/kibana-plugin-core-server.md | 1 + ...in-core-server.savedobjectsfindresponse.md | 2 +- ....savedobjectsfindresponse.saved_objects.md | 2 +- ...ugin-core-server.savedobjectsfindresult.md | 19 +++++++++++++++++++ ...ore-server.savedobjectsfindresult.score.md | 13 +++++++++++++ .../saved_objects/saved_objects_client.ts | 6 ++++-- src/core/server/index.ts | 1 + .../get_sorted_objects_for_export.test.ts | 13 +++++++++++++ .../export/get_sorted_objects_for_export.ts | 7 +++++-- .../routes/integration_tests/find.test.ts | 2 ++ .../service/lib/repository.test.js | 8 +++++--- .../saved_objects/service/lib/repository.ts | 8 ++++++-- .../service/saved_objects_client.ts | 13 ++++++++++++- src/core/server/server.api.md | 7 ++++++- .../server/lib/find_all.test.ts | 5 +++-- .../server/lib/find_relationships.test.ts | 1 + .../apis/saved_objects/find.js | 2 ++ .../apis/saved_objects_management/find.ts | 1 + .../actions/server/actions_client.test.ts | 1 + .../alerts/server/alerts_client.test.ts | 1 + .../case/server/routes/api/utils.test.ts | 15 ++++++++++++--- .../plugins/case/server/routes/api/utils.ts | 4 ++-- ...ypted_saved_objects_client_wrapper.test.ts | 6 ++++++ .../routes/__mocks__/request_responses.ts | 2 ++ .../signals/__mocks__/es_results.ts | 2 +- .../spaces_saved_objects_client.test.ts | 4 ++-- 26 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 147a72016b235..a45bd3d44b28a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -157,6 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md index a1b1a7a056206..4ed069d1598fe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md @@ -20,6 +20,6 @@ export interface SavedObjectsFindResponse | --- | --- | --- | | [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | | [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | -| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObject<T>> | | +| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | | | [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md index adad0dd2b1176..7a91367f6ef0b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md @@ -7,5 +7,5 @@ Signature: ```typescript -saved_objects: Array>; +saved_objects: Array>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md new file mode 100644 index 0000000000000..e455074a7d11b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) + +## SavedObjectsFindResult interface + + +Signature: + +```typescript +export interface SavedObjectsFindResult extends SavedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md new file mode 100644 index 0000000000000..c6646df6ee470 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) + +## SavedObjectsFindResult.score property + +The Elasticsearch `_score` of this result. + +Signature: + +```typescript +score: number; +``` diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 5c8eca4a33ec5..cb279b2cc4c8f 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -303,7 +303,6 @@ export class SavedObjectsClient { query, }); return request.then((resp) => { - resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d)); return renameKeys< PromiseType>, SavedObjectsFindResponsePublic @@ -314,7 +313,10 @@ export class SavedObjectsClient { per_page: 'perPage', page: 'page', }, - resp + { + ...resp, + saved_objects: resp.saved_objects.map((d) => this.createSavedObject(d)), + } ) as SavedObjectsFindResponsePublic; }); }; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 658c24f835020..dccd58c24a7d0 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -217,6 +217,7 @@ export { SavedObjectsErrorHelpers, SavedObjectsExportOptions, SavedObjectsExportResultDetails, + SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, SavedObjectsImportError, diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 32485f461f59b..5da2235828b5c 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -47,6 +47,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -59,6 +60,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -133,6 +135,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -145,6 +148,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -192,6 +196,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -204,6 +209,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -279,6 +285,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -291,6 +298,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -366,6 +374,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { type: 'index-pattern', @@ -378,6 +387,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -405,6 +415,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'baz', }, + score: 1, references: [], }, { @@ -413,6 +424,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'foo', }, + score: 1, references: [], }, { @@ -421,6 +433,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'bar', }, + score: 1, references: [], }, ], diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index cafaa5a3147db..6e985c25aeaef 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -116,8 +116,11 @@ async function fetchObjectsToExport({ } // sorts server-side by _id, since it's only available in fielddata - return findResponse.saved_objects.sort((a: SavedObject, b: SavedObject) => - a.id > b.id ? 1 : -1 + return ( + findResponse.saved_objects + // exclude the find-specific `score` property from the exported objects + .map(({ score, ...obj }) => obj) + .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) ); } else { throw Boom.badRequest('Either `type` or `objects` are required.'); diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts index 31bda1d6b9cbd..33e12dd4e517d 100644 --- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts @@ -79,6 +79,7 @@ describe('GET /api/saved_objects/_find', () => { timeFieldName: '@timestamp', notExpandable: true, attributes: {}, + score: 1, references: [], }, { @@ -88,6 +89,7 @@ describe('GET /api/saved_objects/_find', () => { timeFieldName: '@timestamp', notExpandable: true, attributes: {}, + score: 1, references: [], }, ], diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index d631ef9cb353c..ea749235cbb41 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1939,7 +1939,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, - _score: 1, + _score: 2, ...mockVersionProps, _source: { namespace, @@ -1954,7 +1954,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, - _score: 1, + _score: 3, ...mockVersionProps, _source: { namespace, @@ -1970,7 +1970,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, - _score: 1, + _score: 4, ...mockVersionProps, _source: { type: NAMESPACE_AGNOSTIC_TYPE, @@ -2131,6 +2131,7 @@ describe('SavedObjectsRepository', () => { type: doc._source.type, ...mockTimestampFields, version: mockVersion, + score: doc._score, attributes: doc._source[doc._source.type], references: [], }); @@ -2153,6 +2154,7 @@ describe('SavedObjectsRepository', () => { type: doc._source.type, ...mockTimestampFields, version: mockVersion, + score: doc._score, attributes: doc._source[doc._source.type], references: [], }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 03538f2394845..40c5282a77e49 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -41,6 +41,7 @@ import { SavedObjectsBulkUpdateResponse, SavedObjectsCreateOptions, SavedObjectsFindResponse, + SavedObjectsFindResult, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, SavedObjectsBulkUpdateObject, @@ -674,8 +675,11 @@ export class SavedObjectsRepository { page, per_page: perPage, total: response.hits.total, - saved_objects: response.hits.hits.map((hit: SavedObjectsRawDoc) => - this._rawToSavedObject(hit) + saved_objects: response.hits.hits.map( + (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({ + ...this._rawToSavedObject(hit), + score: (hit as any)._score, + }) ), }; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 8780f07cc3091..e15a92c92772f 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -79,6 +79,17 @@ export interface SavedObjectsBulkResponse { saved_objects: Array>; } +/** + * + * @public + */ +export interface SavedObjectsFindResult extends SavedObject { + /** + * The Elasticsearch `_score` of this result. + */ + score: number; +} + /** * Return type of the Saved Objects `find()` method. * @@ -88,7 +99,7 @@ export interface SavedObjectsBulkResponse { * @public */ export interface SavedObjectsFindResponse { - saved_objects: Array>; + saved_objects: Array>; total: number; per_page: number; page: number; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ecfa09fbd37f3..833c8918a0860 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2039,11 +2039,16 @@ export interface SavedObjectsFindResponse { // (undocumented) per_page: number; // (undocumented) - saved_objects: Array>; + saved_objects: Array>; // (undocumented) total: number; } +// @public (undocumented) +export interface SavedObjectsFindResult extends SavedObject { + score: number; +} + // @public export interface SavedObjectsImportConflictError { // (undocumented) diff --git a/src/plugins/saved_objects_management/server/lib/find_all.test.ts b/src/plugins/saved_objects_management/server/lib/find_all.test.ts index 2515d11f6d4bb..823a103d8bab3 100644 --- a/src/plugins/saved_objects_management/server/lib/find_all.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_all.test.ts @@ -18,17 +18,18 @@ */ import { times } from 'lodash'; -import { SavedObjectsFindOptions, SavedObject } from 'src/core/server'; +import { SavedObjectsFindOptions, SavedObjectsFindResult } from 'src/core/server'; import { savedObjectsClientMock } from '../../../../core/server/mocks'; import { findAll } from './find_all'; describe('findAll', () => { let savedObjectsClient: ReturnType; - const createObj = (id: number): SavedObject => ({ + const createObj = (id: number): SavedObjectsFindResult => ({ type: 'type', id: `id-${id}`, attributes: {}, + score: 1, references: [], }); diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts index 2c8997c9af21a..e18a45d9bdf44 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts @@ -77,6 +77,7 @@ describe('findRelationships', () => { type: 'parent-type', id: 'parent-id', attributes: {}, + score: 1, references: [], }, ], diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 7a57d182bc812..7cb5955e4a43d 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -46,6 +46,7 @@ export default function ({ getService }) { attributes: { title: 'Count of requests', }, + score: 0, migrationVersion: resp.body.saved_objects[0].migrationVersion, references: [ { @@ -134,6 +135,7 @@ export default function ({ getService }) { .searchSourceJSON, }, }, + score: 0, references: [ { name: 'kibanaSavedObjectMeta.searchSourceJSON.index', diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index e15a9e989d21f..4d9f1c1658139 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) { type: 'index-pattern', }, ], + score: 0, updated_at: '2017-09-21T18:51:23.794Z', meta: { editUrl: diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 2865bbbe1d944..69fab828e63de 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -386,6 +386,7 @@ describe('getAll()', () => { foo: 'bar', }, }, + score: 1, references: [], }, ], diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 9685f58b8fb31..f494f1358980d 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1667,6 +1667,7 @@ describe('find()', () => { }, ], }, + score: 1, references: [ { name: 'action_0', diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 81156b98bab83..2da489e643435 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -222,7 +222,12 @@ describe('Utils', () => { ]; const res = transformCases( - { saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 }, + { + saved_objects: mockCases.map((obj) => ({ ...obj, score: 1 })), + total: mockCases.length, + per_page: 10, + page: 1, + }, 2, 2, extraCaseData, @@ -232,7 +237,11 @@ describe('Utils', () => { page: 1, per_page: 10, total: mockCases.length, - cases: flattenCaseSavedObjects(mockCases, extraCaseData, '123'), + cases: flattenCaseSavedObjects( + mockCases.map((obj) => ({ ...obj, score: 1 })), + extraCaseData, + '123' + ), count_open_cases: 2, count_closed_cases: 2, }); @@ -500,7 +509,7 @@ describe('Utils', () => { describe('transformComments', () => { it('transforms correctly', () => { const comments = { - saved_objects: mockCaseComments, + saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })), total: mockCaseComments.length, per_page: 10, page: 1, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index b7f3c68d1662f..ec2881807442f 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -101,7 +101,7 @@ export const transformCases = ( }); export const flattenCaseSavedObjects = ( - savedObjects: SavedObjectsFindResponse['saved_objects'], + savedObjects: Array>, totalCommentByCase: TotalCommentByCase[], caseConfigureConnectorId: string = 'none' ): CaseResponse[] => @@ -146,7 +146,7 @@ export const transformComments = ( }); export const flattenCommentSavedObjects = ( - savedObjects: SavedObjectsFindResponse['saved_objects'] + savedObjects: Array> ): CommentResponse[] => savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject) => { return [...acc, flattenCommentSavedObject(savedObject)]; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 7098f611defa0..ec5d81532e238 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -676,12 +676,14 @@ describe('#find', () => { id: 'some-id', type: 'unknown-type', attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, + score: 1, references: [], }, { id: 'some-id-2', type: 'unknown-type', attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, + score: 1, references: [], }, ], @@ -722,6 +724,7 @@ describe('#find', () => { attrNotSoSecret: 'not-so-secret', attrThree: 'three', }, + score: 1, references: [], }, { @@ -733,6 +736,7 @@ describe('#find', () => { attrNotSoSecret: '*not-so-secret*', attrThree: 'three', }, + score: 1, references: [], }, ], @@ -793,6 +797,7 @@ describe('#find', () => { attrNotSoSecret: 'not-so-secret', attrThree: 'three', }, + score: 1, references: [], }, { @@ -804,6 +809,7 @@ describe('#find', () => { attrNotSoSecret: '*not-so-secret*', attrThree: 'three', }, + score: 1, references: [], }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index fe66496f70dcd..9928ce4807da9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -549,6 +549,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse< searchAfterTimeDurations: ['200.00'], bulkCreateTimeDurations: ['800.43'], }, + score: 1, references: [], updated_at: '2020-02-18T15:26:51.333Z', version: 'WzQ2LDFd', @@ -570,6 +571,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse< searchAfterTimeDurations: ['200.00'], bulkCreateTimeDurations: ['800.43'], }, + score: 1, references: [], updated_at: '2020-02-18T15:15:58.860Z', version: 'WzMyLDFd', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6056e692854af..01ee41e3b877c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -391,7 +391,7 @@ export const exampleFindRuleStatusResponse: ( total: 1, per_page: 6, page: 1, - saved_objects: mockStatuses, + saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })), }); export const mockLogger: Logger = loggingServiceMock.createLogger(); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 75cd501a1a9ae..190429d2dacd4 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -138,7 +138,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; test(`passes options.type to baseClient if valid singular type specified`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { - saved_objects: [createMockResponse()], + saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, per_page: 0, page: 0, @@ -158,7 +158,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; test(`supplements options with the current namespace`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { - saved_objects: [createMockResponse()], + saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, per_page: 0, page: 0, From c8c20e4ca8e768bcce2e471a2f80aef03d4a2a62 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 18 Jun 2020 13:02:56 +0300 Subject: [PATCH 67/68] Add functional test for Kibana embedded in iframe (#68544) * convert kbn test config into TS * add test for Kibana embedded in iframe * run embedded tests in functional suite * ignore tls errors in functional tests by default * switch test to https * remove env vars mutation * allow to pass ssl config to Kibana * pass ssl config to axios * adopt KbnClient interfaces * adopt KibanaServer * use KbnRequester in security service * set sameSiteCookies:None in test * acceptInsecureCerts in chrome * remove leftovers * fix type error * remove unnecessary field * address comments * refactor plugin * refactor test * make acceptInsecureCerts configurable * run firefox tests on ci * up TS version * fix firefox.sh script * fix path Co-authored-by: Elastic Machine --- .../src/kbn_client/kbn_client.ts | 8 +-- .../src/kbn_client/kbn_client_requester.ts | 38 ++++++++--- .../kbn_client/kbn_client_saved_objects.ts | 16 +++-- .../src/kbn_client/kbn_client_status.ts | 3 +- .../src/kbn_client/kbn_client_ui_settings.ts | 7 +- .../lib/config/schema.ts | 9 +++ .../kbn-test/src/kbn/{index.js => index.ts} | 0 ...{kbn_test_config.js => kbn_test_config.ts} | 24 ++++--- .../kbn-test/src/kbn/{users.js => users.ts} | 0 src/es_archiver/es_archiver.ts | 2 +- .../services/kibana_server/kibana_server.ts | 4 +- test/common/services/security/role.ts | 28 ++++---- .../common/services/security/role_mappings.ts | 32 +++------ test/common/services/security/security.ts | 12 ++-- test/common/services/security/user.ts | 34 ++++------ test/functional/services/common/browser.ts | 5 ++ test/functional/services/remote/remote.ts | 14 ++-- test/functional/services/remote/webdriver.ts | 18 +++-- test/scripts/jenkins_xpack_firefox_smoke.sh | 3 +- x-pack/scripts/functional_tests.js | 1 + .../functional_embedded/config.firefox.ts | 27 ++++++++ x-pack/test/functional_embedded/config.ts | 67 +++++++++++++++++++ .../ftr_provider_context.d.ts | 12 ++++ .../plugins/iframe_embedded/kibana.json | 7 ++ .../plugins/iframe_embedded/package.json | 14 ++++ .../plugins/iframe_embedded/server/index.ts | 11 +++ .../plugins/iframe_embedded/server/plugin.ts | 45 +++++++++++++ x-pack/test/functional_embedded/services.ts | 9 +++ .../tests/iframe_embedded.ts | 42 ++++++++++++ .../test/functional_embedded/tests/index.ts | 14 ++++ 30 files changed, 393 insertions(+), 113 deletions(-) rename packages/kbn-test/src/kbn/{index.js => index.ts} (100%) rename packages/kbn-test/src/kbn/{kbn_test_config.js => kbn_test_config.ts} (76%) rename packages/kbn-test/src/kbn/{users.js => users.ts} (100%) create mode 100644 x-pack/test/functional_embedded/config.firefox.ts create mode 100644 x-pack/test/functional_embedded/config.ts create mode 100644 x-pack/test/functional_embedded/ftr_provider_context.d.ts create mode 100644 x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json create mode 100644 x-pack/test/functional_embedded/plugins/iframe_embedded/package.json create mode 100644 x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts create mode 100644 x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts create mode 100644 x-pack/test/functional_embedded/services.ts create mode 100644 x-pack/test/functional_embedded/tests/iframe_embedded.ts create mode 100644 x-pack/test/functional_embedded/tests/index.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts index 2eb6c6cc5aac6..861ea0988692c 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts @@ -18,7 +18,7 @@ */ import { ToolingLog } from '../tooling_log'; -import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; +import { KibanaConfig, KbnClientRequester, ReqOptions } from './kbn_client_requester'; import { KbnClientStatus } from './kbn_client_status'; import { KbnClientPlugins } from './kbn_client_plugins'; import { KbnClientVersion } from './kbn_client_version'; @@ -26,7 +26,7 @@ import { KbnClientSavedObjects } from './kbn_client_saved_objects'; import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; export class KbnClient { - private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls); + private readonly requester = new KbnClientRequester(this.log, this.kibanaConfig); readonly status = new KbnClientStatus(this.requester); readonly plugins = new KbnClientPlugins(this.status); readonly version = new KbnClientVersion(this.status); @@ -43,10 +43,10 @@ export class KbnClient { */ constructor( private readonly log: ToolingLog, - private readonly kibanaUrls: string[], + private readonly kibanaConfig: KibanaConfig, private readonly uiSettingDefaults?: UiSettingValues ) { - if (!kibanaUrls.length) { + if (!kibanaConfig.url) { throw new Error('missing Kibana urls'); } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index ea4159de55749..2aba2be56f277 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - import Url from 'url'; - -import Axios from 'axios'; +import Https from 'https'; +import Axios, { AxiosResponse } from 'axios'; import { isAxiosRequestError, isAxiosResponseError } from '../axios'; import { ToolingLog } from '../tooling_log'; @@ -70,20 +69,38 @@ const delay = (ms: number) => setTimeout(resolve, ms); }); +export interface KibanaConfig { + url: string; + ssl?: { + enabled: boolean; + key: string; + certificate: string; + certificateAuthorities: string; + }; +} + export class KbnClientRequester { - constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {} + private readonly httpsAgent: Https.Agent | null; + constructor(private readonly log: ToolingLog, private readonly kibanaConfig: KibanaConfig) { + this.httpsAgent = + kibanaConfig.ssl && kibanaConfig.ssl.enabled + ? new Https.Agent({ + cert: kibanaConfig.ssl.certificate, + key: kibanaConfig.ssl.key, + ca: kibanaConfig.ssl.certificateAuthorities, + }) + : null; + } private pickUrl() { - const url = this.kibanaUrls.shift()!; - this.kibanaUrls.push(url); - return url; + return this.kibanaConfig.url; } public resolveUrl(relativeUrl: string = '/') { return Url.resolve(this.pickUrl(), relativeUrl); } - async request(options: ReqOptions): Promise { + async request(options: ReqOptions): Promise> { const url = Url.resolve(this.pickUrl(), options.path); const description = options.description || `${options.method} ${url}`; let attempt = 0; @@ -93,7 +110,7 @@ export class KbnClientRequester { attempt += 1; try { - const response = await Axios.request({ + const response = await Axios.request({ method: options.method, url, data: options.body, @@ -101,9 +118,10 @@ export class KbnClientRequester { headers: { 'kbn-xsrf': 'kbn-client', }, + httpsAgent: this.httpsAgent, }); - return response.data; + return response; } catch (error) { const conflictOnGet = isConcliftOnGetError(error); const requestedRetries = options.retries !== undefined; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts index e671061b34352..7334c6353debf 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts @@ -71,12 +71,13 @@ export class KbnClientSavedObjects { public async migrate() { this.log.debug('Migrating saved objects'); - return await this.requester.request({ + const { data } = await this.requester.request({ description: 'migrate saved objects', path: uriencode`/internal/saved_objects/_migrate`, method: 'POST', body: {}, }); + return data; } /** @@ -85,11 +86,12 @@ export class KbnClientSavedObjects { public async get>(options: GetOptions) { this.log.debug('Gettings saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'get saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'GET', }); + return data; } /** @@ -98,7 +100,7 @@ export class KbnClientSavedObjects { public async create>(options: IndexOptions) { this.log.debug('Creating saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'update saved object', path: options.id ? uriencode`/api/saved_objects/${options.type}/${options.id}` @@ -113,6 +115,7 @@ export class KbnClientSavedObjects { references: options.references, }, }); + return data; } /** @@ -121,7 +124,7 @@ export class KbnClientSavedObjects { public async update>(options: UpdateOptions) { this.log.debug('Updating saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'update saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, query: { @@ -134,6 +137,7 @@ export class KbnClientSavedObjects { references: options.references, }, }); + return data; } /** @@ -142,10 +146,12 @@ export class KbnClientSavedObjects { public async delete(options: GetOptions) { this.log.debug('Deleting saved object %s/%s', options); - return await this.requester.request({ + const { data } = await this.requester.request({ description: 'delete saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'DELETE', }); + + return data; } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts index 22baf4a330416..4f203e73620f3 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts @@ -52,10 +52,11 @@ export class KbnClientStatus { * Get the full server status */ async get() { - return await this.requester.request({ + const { data } = await this.requester.request({ method: 'GET', path: 'api/status', }); + return data; } /** diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index dbfa87e70032b..6ee2d3bfe59b0 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -57,10 +57,11 @@ export class KbnClientUiSettings { * Unset a uiSetting */ async unset(setting: string) { - return await this.requester.request({ + const { data } = await this.requester.request({ path: uriencode`/api/kibana/settings/${setting}`, method: 'DELETE', }); + return data; } /** @@ -105,11 +106,11 @@ export class KbnClientUiSettings { } private async getAll() { - const resp = await this.requester.request({ + const { data } = await this.requester.request({ path: '/api/kibana/settings', method: 'GET', }); - return resp.settings; + return data.settings; } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 29ec28175a851..e9aeee87f1a3b 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -38,6 +38,14 @@ const urlPartsSchema = () => password: Joi.string(), pathname: Joi.string().regex(/^\//, 'start with a /'), hash: Joi.string().regex(/^\//, 'start with a /'), + ssl: Joi.object() + .keys({ + enabled: Joi.boolean().default(false), + certificate: Joi.string().optional(), + certificateAuthorities: Joi.string().optional(), + key: Joi.string().optional(), + }) + .default(), }) .default(); @@ -122,6 +130,7 @@ export const schema = Joi.object() type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'), logPollingMs: Joi.number().default(100), + acceptInsecureCerts: Joi.boolean().default(false), }) .default(), diff --git a/packages/kbn-test/src/kbn/index.js b/packages/kbn-test/src/kbn/index.ts similarity index 100% rename from packages/kbn-test/src/kbn/index.js rename to packages/kbn-test/src/kbn/index.ts diff --git a/packages/kbn-test/src/kbn/kbn_test_config.js b/packages/kbn-test/src/kbn/kbn_test_config.ts similarity index 76% rename from packages/kbn-test/src/kbn/kbn_test_config.js rename to packages/kbn-test/src/kbn/kbn_test_config.ts index c43efabb4b747..909c94098cf5d 100644 --- a/packages/kbn-test/src/kbn/kbn_test_config.js +++ b/packages/kbn-test/src/kbn/kbn_test_config.ts @@ -16,26 +16,34 @@ * specific language governing permissions and limitations * under the License. */ - -import { kibanaTestUser } from './users'; import url from 'url'; +import { kibanaTestUser } from './users'; + +interface UrlParts { + protocol?: string; + hostname?: string; + port?: number; + auth?: string; + username?: string; + password?: string; +} export const kbnTestConfig = new (class KbnTestConfig { getPort() { return this.getUrlParts().port; } - getUrlParts() { + getUrlParts(): UrlParts { // allow setting one complete TEST_KIBANA_URL for ES like https://elastic:changeme@example.com:9200 if (process.env.TEST_KIBANA_URL) { const testKibanaUrl = url.parse(process.env.TEST_KIBANA_URL); return { - protocol: testKibanaUrl.protocol.slice(0, -1), + protocol: testKibanaUrl.protocol?.slice(0, -1), hostname: testKibanaUrl.hostname, - port: parseInt(testKibanaUrl.port, 10), + port: testKibanaUrl.port ? parseInt(testKibanaUrl.port, 10) : undefined, auth: testKibanaUrl.auth, - username: testKibanaUrl.auth.split(':')[0], - password: testKibanaUrl.auth.split(':')[1], + username: testKibanaUrl.auth?.split(':')[0], + password: testKibanaUrl.auth?.split(':')[1], }; } @@ -44,7 +52,7 @@ export const kbnTestConfig = new (class KbnTestConfig { return { protocol: process.env.TEST_KIBANA_PROTOCOL || 'http', hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost', - port: parseInt(process.env.TEST_KIBANA_PORT, 10) || 5620, + port: process.env.TEST_KIBANA_PORT ? parseInt(process.env.TEST_KIBANA_PORT, 10) : 5620, auth: `${username}:${password}`, username, password, diff --git a/packages/kbn-test/src/kbn/users.js b/packages/kbn-test/src/kbn/users.ts similarity index 100% rename from packages/kbn-test/src/kbn/users.js rename to packages/kbn-test/src/kbn/users.ts diff --git a/src/es_archiver/es_archiver.ts b/src/es_archiver/es_archiver.ts index f36cbb3f516b9..e335652195b86 100644 --- a/src/es_archiver/es_archiver.ts +++ b/src/es_archiver/es_archiver.ts @@ -49,7 +49,7 @@ export class EsArchiver { this.client = client; this.dataDir = dataDir; this.log = log; - this.kbnClient = new KbnClient(log, [kibanaUrl]); + this.kbnClient = new KbnClient(log, { url: kibanaUrl }); } /** diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 16039d6fee833..4a251cca044d3 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -27,9 +27,9 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) { const config = getService('config'); const lifecycle = getService('lifecycle'); const url = Url.format(config.get('servers.kibana')); + const ssl = config.get('servers.kibana').ssl; const defaults = config.get('uiSettings.defaults'); - - const kbn = new KbnClient(log, [url], defaults); + const kbn = new KbnClient(log, { url, ssl }, defaults); if (defaults) { lifecycle.beforeTests.add(async () => { diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index dfc6ff9b164e5..caa5549a70f0c 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -17,27 +17,20 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class Role { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kibanaServer: KbnClient) {} public async create(name: string, role: any) { this.log.debug(`creating role ${name}`); - const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role); + const { data, status, statusText } = await this.kibanaServer.request({ + path: `/api/security/role/${name}`, + method: 'PUT', + body: role, + retries: 0, + }); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` @@ -47,7 +40,10 @@ export class Role { public async delete(name: string) { this.log.debug(`deleting role ${name}`); - const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`); + const { data, status, statusText } = await this.kibanaServer.request({ + path: `/api/security/role/${name}`, + method: 'DELETE', + }); if (status !== 204 && status !== 404) { throw new Error( `Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect( diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index cc2fa23825498..7951d4b5b47b2 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -17,30 +17,19 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class RoleMappings { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role_mappings' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kbnClient: KbnClient) {} public async create(name: string, roleMapping: Record) { this.log.debug(`creating role mapping ${name}`); - const { data, status, statusText } = await this.axios.post( - `/internal/security/role_mapping/${name}`, - roleMapping - ); + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/role_mapping/${name}`, + method: 'POST', + body: roleMapping, + }); if (status !== 200) { throw new Error( `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` @@ -51,9 +40,10 @@ export class RoleMappings { public async delete(name: string) { this.log.debug(`deleting role mapping ${name}`); - const { data, status, statusText } = await this.axios.delete( - `/internal/security/role_mapping/${name}` - ); + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/role_mapping/${name}`, + method: 'DELETE', + }); if (status !== 200 && status !== 404) { throw new Error( `Expected status code of 200 or 404, received ${status} ${statusText}: ${util.inspect( diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 6ad0933a2a5a2..fae4c9198cab6 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -17,8 +17,6 @@ * under the License. */ -import { format as formatUrl } from 'url'; - import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; @@ -28,14 +26,14 @@ import { createTestUserService } from './test_user'; export async function SecurityServiceProvider(context: FtrProviderContext) { const { getService } = context; const log = getService('log'); - const config = getService('config'); - const url = formatUrl(config.get('servers.kibana')); - const role = new Role(url, log); - const user = new User(url, log); + const kibanaServer = getService('kibanaServer'); + + const role = new Role(log, kibanaServer); + const user = new User(log, kibanaServer); const testUser = await createTestUserService(role, user, context); return new (class SecurityService { - roleMappings = new RoleMappings(url, log); + roleMappings = new RoleMappings(log, kibanaServer); testUser = testUser; role = role; user = user; diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index ae02127043234..58c4d0f1cf34e 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -17,33 +17,22 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class User { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kbnClient: KbnClient) {} public async create(username: string, user: any) { this.log.debug(`creating user ${username}`); - const { data, status, statusText } = await this.axios.post( - `/internal/security/users/${username}`, - { + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/users/${username}`, + method: 'POST', + body: { username, ...user, - } - ); + }, + }); if (status !== 200) { throw new Error( `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` @@ -54,9 +43,10 @@ export class User { public async delete(username: string) { this.log.debug(`deleting user ${username}`); - const { data, status, statusText } = await this.axios.delete( - `/internal/security/users/${username}` - ); + const { data, status, statusText } = await await this.kbnClient.request({ + path: `/internal/security/users/${username}`, + method: 'DELETE', + }); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 3297f6e094f7c..d6a4fc91481de 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -529,5 +529,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { await driver.executeScript('document.body.scrollLeft = ' + scrollSize); return this.getScrollLeft(); } + + public async switchToFrame(idOrElement: number | WebElementWrapper) { + const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement; + await driver.switchTo().frame(_id); + } })(); } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 5a3a775cae0c5..99643929c4682 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -23,7 +23,7 @@ import { resolve } from 'path'; import { mergeMap } from 'rxjs/operators'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { initWebDriver } from './webdriver'; +import { initWebDriver, BrowserConfig } from './webdriver'; import { Browsers } from './browsers'; export async function RemoteProvider({ getService }: FtrProviderContext) { @@ -58,12 +58,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2)); }; - const { driver, consoleLog$ } = await initWebDriver( - log, - browserType, - lifecycle, - config.get('browser.logPollingMs') - ); + const browserConfig: BrowserConfig = { + logPollingMs: config.get('browser.logPollingMs'), + acceptInsecureCerts: config.get('browser.acceptInsecureCerts'), + }; + + const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig); const isW3CEnabled = (driver as any).executor_.w3c; const caps = await driver.getCapabilities(); diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 9fbbf28bbf42c..27814060e70c1 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -73,13 +73,18 @@ Executor.prototype.execute = preventParallelCalls( (command: { getName: () => string }) => NO_QUEUE_COMMANDS.includes(command.getName()) ); +export interface BrowserConfig { + logPollingMs: number; + acceptInsecureCerts: boolean; +} + let attemptCounter = 0; let edgePaths: { driverPath: string | undefined; browserPath: string | undefined }; async function attemptToCreateCommand( log: ToolingLog, browserType: Browsers, lifecycle: Lifecycle, - logPollingMs: number + config: BrowserConfig ) { const attemptId = ++attemptCounter; log.debug('[webdriver] Creating session'); @@ -114,6 +119,7 @@ async function attemptToCreateCommand( if (certValidation === '0') { chromeOptions.push('ignore-certificate-errors'); } + if (remoteDebug === '1') { // Visit chrome://inspect in chrome to remotely view/debug chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222'); @@ -125,6 +131,7 @@ async function attemptToCreateCommand( }); chromeCapabilities.set('unexpectedAlertBehaviour', 'accept'); chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' }); + chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts); const session = await new Builder() .forBrowser(browserType) @@ -137,7 +144,7 @@ async function attemptToCreateCommand( consoleLog$: pollForLogEntry$( session, logging.Type.BROWSER, - logPollingMs, + config.logPollingMs, lifecycle.cleanup.after$ ).pipe( takeUntil(lifecycle.cleanup.after$), @@ -174,7 +181,7 @@ async function attemptToCreateCommand( consoleLog$: pollForLogEntry$( session, logging.Type.BROWSER, - logPollingMs, + config.logPollingMs, lifecycle.cleanup.after$ ).pipe( takeUntil(lifecycle.cleanup.after$), @@ -206,6 +213,7 @@ async function attemptToCreateCommand( 'browser.helperApps.neverAsk.saveToDisk', 'application/comma-separated-values, text/csv, text/plain' ); + firefoxOptions.setAcceptInsecureCerts(config.acceptInsecureCerts); if (headlessBrowser === '1') { // See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode @@ -317,7 +325,7 @@ export async function initWebDriver( log: ToolingLog, browserType: Browsers, lifecycle: Lifecycle, - logPollingMs: number + config: BrowserConfig ) { const logger = getLogger('webdriver.http.Executor'); logger.setLevel(logging.Level.FINEST); @@ -348,7 +356,7 @@ export async function initWebDriver( while (true) { const command = await Promise.race([ delay(30 * SECOND), - attemptToCreateCommand(log, browserType, lifecycle, logPollingMs), + attemptToCreateCommand(log, browserType, lifecycle, config), ]); if (!command) { diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index fdaee76cafa9d..ae924a5e10552 100755 --- a/test/scripts/jenkins_xpack_firefox_smoke.sh +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -7,4 +7,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \ --debug --bail \ --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js; + --config test/functional/config.firefox.js \ + --config test/functional_embedded/config.firefox.ts; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 37b22a687741e..6cafa3eeef08e 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -51,6 +51,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/config.js'), + require.resolve('../test/functional_embedded/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/functional_embedded/config.firefox.ts new file mode 100644 index 0000000000000..2051d1afd4ab3 --- /dev/null +++ b/x-pack/test/functional_embedded/config.firefox.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const chromeConfig = await readConfigFile(require.resolve('./config')); + + return { + ...chromeConfig.getAll(), + + browser: { + type: 'firefox', + acceptInsecureCerts: true, + }, + + suiteTags: { + exclude: ['skipFirefox'], + }, + + junit: { + reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security', + }, + }; +} diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts new file mode 100644 index 0000000000000..95b290ece7db2 --- /dev/null +++ b/x-pack/test/functional_embedded/config.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Fs from 'fs'; +import { resolve } from 'path'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from '../functional/page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded'); + + const servers = { + ...kibanaFunctionalConfig.get('servers'), + elasticsearch: { + ...kibanaFunctionalConfig.get('servers.elasticsearch'), + }, + kibana: { + ...kibanaFunctionalConfig.get('servers.kibana'), + protocol: 'https', + ssl: { + enabled: true, + key: Fs.readFileSync(KBN_KEY_PATH).toString('utf8'), + certificate: Fs.readFileSync(KBN_CERT_PATH).toString('utf8'), + certificateAuthorities: Fs.readFileSync(CA_CERT_PATH).toString('utf8'), + }, + }, + }; + + return { + testFiles: [require.resolve('./tests')], + servers, + services: kibanaFunctionalConfig.get('services'), + pageObjects, + browser: { + acceptInsecureCerts: true, + }, + junit: { + reportName: 'Kibana Embedded in iframe with X-Pack Security', + }, + + esTestCluster: kibanaFunctionalConfig.get('esTestCluster'), + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + kbnTestServer: { + ...kibanaFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${iframeEmbeddedPlugin}`, + '--server.ssl.enabled=true', + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, + + '--xpack.security.sameSiteCookies=None', + '--xpack.security.secureCookies=true', + ], + }, + }; +} diff --git a/x-pack/test/functional_embedded/ftr_provider_context.d.ts b/x-pack/test/functional_embedded/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..5646c06a3cd30 --- /dev/null +++ b/x-pack/test/functional_embedded/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json new file mode 100644 index 0000000000000..ea9f55bd21c6e --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "iframe_embedded", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json new file mode 100644 index 0000000000000..9fa1554e5312b --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json @@ -0,0 +1,14 @@ +{ + "name": "iframe_embedded", + "version": "0.0.0", + "kibana": { + "version": "kibana" + }, + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts new file mode 100644 index 0000000000000..976ef19d4d8a7 --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/server'; +import { IframeEmbeddedPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new IframeEmbeddedPlugin(initContext); diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts new file mode 100644 index 0000000000000..890fe14cf03cf --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Url from 'url'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; + +function renderBody(iframeUrl: string) { + return ` + + + + + Kibana embedded in iframe + + +