diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh
index a9fbe781915b6..5b4a94be50fa2 100755
--- a/.ci/packer_cache_for_branch.sh
+++ b/.ci/packer_cache_for_branch.sh
@@ -46,7 +46,7 @@ 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 \
+ .chromium \
.es \
.chromedriver \
.geckodriver;
diff --git a/.eslintignore b/.eslintignore
index 9de2cc2872960..4b5e781c26971 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,7 @@
**/*.js.snap
**/graphql/types.ts
/.es
+/.chromium
/build
/built_assets
/config/apm.dev.js
diff --git a/.gitignore b/.gitignore
index 32377ec0f1ffe..716cea937f9c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
.signing-config.json
.ackrc
/.es
+/.chromium
.DS_Store
.node_binaries
.native_modules
diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc
index f6bc83d4086c2..97fdcd3e13de9 100644
--- a/docs/apm/api.asciidoc
+++ b/docs/apm/api.asciidoc
@@ -398,7 +398,7 @@ include::api.asciidoc[tag=using-the-APIs]
[%collapsible%open]
======
`version` :::
- (required, string) Name of service.
+ (required, string) Version of service.
`environment` :::
(optional, string) Environment of service.
diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
index 0e2b9bd60ab67..b88a179c5c4b3 100644
--- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
+++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
@@ -19,5 +19,6 @@ export interface DiscoveredPlugin
| [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) | ConfigPath
| Root configuration path used by the plugin, defaults to "id" in snake\_case format. |
| [id](./kibana-plugin-core-server.discoveredplugin.id.md) | PluginName
| Identifier of the plugin. |
| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.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. |
+| [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName[]
| List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins
. |
| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName[]
| An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md
new file mode 100644
index 0000000000000..6d54adb5236ea
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md)
+
+## DiscoveredPlugin.requiredBundles property
+
+List of plugin ids that this plugin's UI code imports modules from that are not in `requiredPlugins`.
+
+Signature:
+
+```typescript
+readonly requiredBundles: readonly PluginName[];
+```
+
+## Remarks
+
+The plugins listed here will be loaded in the browser, even if the plugin is disabled. Required by `@kbn/optimizer` to support cross-plugin imports. "core" and plugins already listed in `requiredPlugins` do not need to be duplicated here.
+
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 5edee51d6c523..6db2f89590149 100644
--- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md
@@ -25,6 +25,7 @@ Should never be used in code outside of Core but is exported for documentation p
| [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. |
+| [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string[]
| List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins
. |
| [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[]
| An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. |
| [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean
| Specifies whether plugin includes some server-side specific functionality. |
| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean
| Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js
file. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md
new file mode 100644
index 0000000000000..98505d07101fe
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md)
+
+## PluginManifest.requiredBundles property
+
+List of plugin ids that this plugin's UI code imports modules from that are not in `requiredPlugins`.
+
+Signature:
+
+```typescript
+readonly requiredBundles: readonly string[];
+```
+
+## Remarks
+
+The plugins listed here will be loaded in the browser, even if the plugin is disabled. Required by `@kbn/optimizer` to support cross-plugin imports. "core" and plugins already listed in `requiredPlugins` do not need to be duplicated here.
+
diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc
index 4123912b79237..6acdbbe3f0a99 100644
--- a/docs/user/reporting/index.asciidoc
+++ b/docs/user/reporting/index.asciidoc
@@ -19,7 +19,7 @@ image::user/reporting/images/share-button.png["Share"]
[float]
== Setup
-{reporting} is automatically enabled in {kib}. The first time {kib} runs, it extracts a custom build for the Chromium web browser, which
+{reporting} is automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which
runs on the server in headless mode to load {kib} and capture the rendered {kib} charts as images.
Chromium is an open-source project not related to Elastic, but the Chromium binary for {kib} has been custom-built by Elastic to ensure it
diff --git a/examples/bfetch_explorer/kibana.json b/examples/bfetch_explorer/kibana.json
index 0039e9647bf83..f32cdfc13a1fe 100644
--- a/examples/bfetch_explorer/kibana.json
+++ b/examples/bfetch_explorer/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["bfetch", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/dashboard_embeddable_examples/kibana.json b/examples/dashboard_embeddable_examples/kibana.json
index bb2ced569edb5..807229fad9dcf 100644
--- a/examples/dashboard_embeddable_examples/kibana.json
+++ b/examples/dashboard_embeddable_examples/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "embeddableExamples", "dashboard", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["esUiShared"]
}
diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json
index 8ae04c1f6c644..771c19cfdbd3d 100644
--- a/examples/embeddable_examples/kibana.json
+++ b/examples/embeddable_examples/kibana.json
@@ -6,5 +6,6 @@
"ui": true,
"requiredPlugins": ["embeddable", "uiActions"],
"optionalPlugins": [],
- "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"]
+ "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/state_containers_examples/kibana.json b/examples/state_containers_examples/kibana.json
index 66da207cb4e77..58346af8f1d19 100644
--- a/examples/state_containers_examples/kibana.json
+++ b/examples/state_containers_examples/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["navigation", "data", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/examples/ui_action_examples/kibana.json b/examples/ui_action_examples/kibana.json
index cd12442daf61c..0e0b6b6830b95 100644
--- a/examples/ui_action_examples/kibana.json
+++ b/examples/ui_action_examples/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActions"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/ui_actions_explorer/kibana.json b/examples/ui_actions_explorer/kibana.json
index f57072e89b06d..0a55e60374710 100644
--- a/examples/ui_actions_explorer/kibana.json
+++ b/examples/ui_actions_explorer/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActions", "uiActionsExamples", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/package.json b/package.json
index 1a497a2ec8b10..a45f240ce13af 100644
--- a/package.json
+++ b/package.json
@@ -127,7 +127,7 @@
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "7.8.0",
"@elastic/ems-client": "7.9.3",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "^2.5.0",
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
index 20c8046daa65e..33f53e336598d 100644
--- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
@@ -1,4 +1,5 @@
{
"id": "bar",
- "ui": true
+ "ui": true,
+ "requiredBundles": ["foo"]
}
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss
new file mode 100644
index 0000000000000..2c1b9562b9567
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss
@@ -0,0 +1,3 @@
+p {
+ background-color: rebeccapurple;
+}
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
index e71a2d485a2f8..1dc7bbe9daeb0 100644
--- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
@@ -1,3 +1,5 @@
+@import "./other_styles.scss";
+
body {
width: $globalStyleConstant;
background-image: url("ui/icon.svg");
diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts
index 0916f12a7110d..9d3f4b88a258f 100644
--- a/packages/kbn-optimizer/src/cli.ts
+++ b/packages/kbn-optimizer/src/cli.ts
@@ -87,6 +87,11 @@ run(
throw createFlagError('expected --report-stats to have no value');
}
+ const filter = typeof flags.filter === 'string' ? [flags.filter] : flags.filter;
+ if (!Array.isArray(filter) || !filter.every((f) => typeof f === 'string')) {
+ throw createFlagError('expected --filter to be one or more strings');
+ }
+
const config = OptimizerConfig.create({
repoRoot: REPO_ROOT,
watch,
@@ -99,6 +104,7 @@ run(
extraPluginScanDirs,
inspectWorkers,
includeCoreBundle,
+ filter,
});
let update$ = runOptimizer(config);
@@ -128,12 +134,13 @@ run(
'inspect-workers',
'report-stats',
],
- string: ['workers', 'scan-dir'],
+ string: ['workers', 'scan-dir', 'filter'],
default: {
core: true,
examples: true,
cache: true,
'inspect-workers': true,
+ filter: [],
},
help: `
--watch run the optimizer in watch mode
@@ -142,6 +149,7 @@ run(
--profile profile the webpack builds and write stats.json files to build outputs
--no-core disable generating the core bundle
--no-cache disable the cache
+ --filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported
--no-examples don't build the example plugins
--dist create bundles that are suitable for inclusion in the Kibana distributable
--scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary)
diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts
index b209bbca25ac4..6197a08485854 100644
--- a/packages/kbn-optimizer/src/common/bundle.test.ts
+++ b/packages/kbn-optimizer/src/common/bundle.test.ts
@@ -50,6 +50,7 @@ it('creates cache keys', () => {
"spec": Object {
"contextDir": "/foo/bar",
"id": "bar",
+ "manifestPath": undefined,
"outputDir": "/foo/bar/target",
"publicDirNames": Array [
"public",
@@ -85,6 +86,7 @@ it('parses bundles from JSON specs', () => {
},
"contextDir": "/foo/bar",
"id": "bar",
+ "manifestPath": undefined,
"outputDir": "/foo/bar/target",
"publicDirNames": Array [
"public",
diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts
index 80af94c30f8da..a354da7a21521 100644
--- a/packages/kbn-optimizer/src/common/bundle.ts
+++ b/packages/kbn-optimizer/src/common/bundle.ts
@@ -18,6 +18,7 @@
*/
import Path from 'path';
+import Fs from 'fs';
import { BundleCache } from './bundle_cache';
import { UnknownVals } from './ts_helpers';
@@ -25,6 +26,11 @@ import { includes, ascending, entriesToObject } from './array_helpers';
const VALID_BUNDLE_TYPES = ['plugin' as const, 'entry' as const];
+const DEFAULT_IMPLICIT_BUNDLE_DEPS = ['core'];
+
+const isStringArray = (input: any): input is string[] =>
+ Array.isArray(input) && input.every((x) => typeof x === 'string');
+
export interface BundleSpec {
readonly type: typeof VALID_BUNDLE_TYPES[0];
/** Unique id for this bundle */
@@ -37,6 +43,8 @@ export interface BundleSpec {
readonly sourceRoot: string;
/** Absolute path to the directory where output should be written */
readonly outputDir: string;
+ /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */
+ readonly manifestPath?: string;
}
export class Bundle {
@@ -56,6 +64,12 @@ export class Bundle {
public readonly sourceRoot: BundleSpec['sourceRoot'];
/** Absolute path to the output directory for this bundle */
public readonly outputDir: BundleSpec['outputDir'];
+ /**
+ * Absolute path to a manifest file with "requiredBundles" which will be
+ * used to allow bundleRefs from this bundle to the exports of another bundle.
+ * Every bundle mentioned in the `requiredBundles` must be built together.
+ */
+ public readonly manifestPath: BundleSpec['manifestPath'];
public readonly cache: BundleCache;
@@ -66,6 +80,7 @@ export class Bundle {
this.contextDir = spec.contextDir;
this.sourceRoot = spec.sourceRoot;
this.outputDir = spec.outputDir;
+ this.manifestPath = spec.manifestPath;
this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache'));
}
@@ -96,8 +111,54 @@ export class Bundle {
contextDir: this.contextDir,
sourceRoot: this.sourceRoot,
outputDir: this.outputDir,
+ manifestPath: this.manifestPath,
};
}
+
+ readBundleDeps(): { implicit: string[]; explicit: string[] } {
+ if (!this.manifestPath) {
+ return {
+ implicit: [...DEFAULT_IMPLICIT_BUNDLE_DEPS],
+ explicit: [],
+ };
+ }
+
+ let json: string;
+ try {
+ json = Fs.readFileSync(this.manifestPath, 'utf8');
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ throw error;
+ }
+
+ json = '{}';
+ }
+
+ let parsedManifest: { requiredPlugins?: string[]; requiredBundles?: string[] };
+ try {
+ parsedManifest = JSON.parse(json);
+ } catch (error) {
+ throw new Error(
+ `unable to parse manifest at [${this.manifestPath}], error: [${error.message}]`
+ );
+ }
+
+ if (typeof parsedManifest === 'object' && parsedManifest) {
+ const explicit = parsedManifest.requiredBundles || [];
+ const implicit = [...DEFAULT_IMPLICIT_BUNDLE_DEPS, ...(parsedManifest.requiredPlugins || [])];
+
+ if (isStringArray(explicit) && isStringArray(implicit)) {
+ return {
+ explicit,
+ implicit,
+ };
+ }
+ }
+
+ throw new Error(
+ `Expected "requiredBundles" and "requiredPlugins" in manifest file [${this.manifestPath}] to be arrays of strings`
+ );
+ }
}
/**
@@ -152,6 +213,13 @@ export function parseBundles(json: string) {
throw new Error('`bundles[]` must have an absolute path `outputDir` property');
}
+ const { manifestPath } = spec;
+ if (manifestPath !== undefined) {
+ if (!(typeof manifestPath === 'string' && Path.isAbsolute(manifestPath))) {
+ throw new Error('`bundles[]` must have an absolute path `manifestPath` property');
+ }
+ }
+
return new Bundle({
type,
id,
@@ -159,6 +227,7 @@ export function parseBundles(json: string) {
contextDir,
sourceRoot,
outputDir,
+ manifestPath,
});
}
);
diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts
index 5ae3e4c28a201..7607e270b5b4f 100644
--- a/packages/kbn-optimizer/src/common/bundle_cache.ts
+++ b/packages/kbn-optimizer/src/common/bundle_cache.ts
@@ -24,6 +24,7 @@ export interface State {
optimizerCacheKey?: unknown;
cacheKey?: unknown;
moduleCount?: number;
+ workUnits?: number;
files?: string[];
bundleRefExportIds?: string[];
}
@@ -96,6 +97,10 @@ export class BundleCache {
return this.get().cacheKey;
}
+ public getWorkUnits() {
+ return this.get().workUnits;
+ }
+
public getOptimizerCacheKey() {
return this.get().optimizerCacheKey;
}
diff --git a/packages/kbn-optimizer/src/common/bundle_refs.ts b/packages/kbn-optimizer/src/common/bundle_refs.ts
index a5c60f2031c0b..85731f32f8991 100644
--- a/packages/kbn-optimizer/src/common/bundle_refs.ts
+++ b/packages/kbn-optimizer/src/common/bundle_refs.ts
@@ -114,6 +114,10 @@ export class BundleRefs {
constructor(private readonly refs: BundleRef[]) {}
+ public forBundleIds(bundleIds: string[]) {
+ return this.refs.filter((r) => bundleIds.includes(r.bundleId));
+ }
+
public filterByExportIds(exportIds: string[]) {
return this.refs.filter((r) => exportIds.includes(r.exportId));
}
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 211cfac3806ad..c52873ab7ec20 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
@@ -10,6 +10,7 @@ OptimizerConfig {
},
"contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar,
"id": "bar",
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
"outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public,
"publicDirNames": Array [
"public",
@@ -24,6 +25,7 @@ OptimizerConfig {
},
"contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo,
"id": "foo",
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
"outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public,
"publicDirNames": Array [
"public",
@@ -42,18 +44,21 @@ OptimizerConfig {
"extraPublicDirs": Array [],
"id": "bar",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo,
"extraPublicDirs": Array [],
"id": "foo",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz,
"extraPublicDirs": Array [],
"id": "baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json,
},
],
"profileWebpack": false,
@@ -66,7 +71,7 @@ OptimizerConfig {
}
`;
-exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i {
expect(foo.cache.getModuleCount()).toBe(6);
expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(`
Array [
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
/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,
@@ -160,12 +161,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
Array [
/node_modules/css-loader/package.json,
/node_modules/style-loader/package.json,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss,
/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/_other_styles.scss,
/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/src/legacy/ui/public/icon.svg,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss,
/packages/kbn-optimizer/target/worker/entry_point_creator.js,
+ /packages/kbn-optimizer/target/worker/postcss.config.js,
/packages/kbn-ui-shared-deps/public_path_module_creator.js,
]
`);
diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts
index 23767be610da4..20d98f74dbe86 100644
--- a/packages/kbn-optimizer/src/log_optimizer_state.ts
+++ b/packages/kbn-optimizer/src/log_optimizer_state.ts
@@ -54,12 +54,18 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
if (event?.type === 'worker started') {
let moduleCount = 0;
+ let workUnits = 0;
for (const bundle of event.bundles) {
moduleCount += bundle.cache.getModuleCount() ?? NaN;
+ workUnits += bundle.cache.getWorkUnits() ?? NaN;
}
- const mcString = isFinite(moduleCount) ? String(moduleCount) : '?';
- const bcString = String(event.bundles.length);
- log.info(`starting worker [${bcString} bundles, ${mcString} modules]`);
+
+ log.info(
+ `starting worker [${event.bundles.length} ${
+ event.bundles.length === 1 ? 'bundle' : 'bundles'
+ }]`
+ );
+ log.debug(`modules [${moduleCount}] work units [${workUnits}]`);
}
if (state.phase === 'reallocating') {
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 ca50a49e26913..5443a88eb1a63 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
@@ -23,11 +23,11 @@ import { Bundle } from '../common';
import { assignBundlesToWorkers, Assignments } from './assign_bundles_to_workers';
-const hasModuleCount = (b: Bundle) => b.cache.getModuleCount() !== undefined;
-const noModuleCount = (b: Bundle) => b.cache.getModuleCount() === undefined;
+const hasWorkUnits = (b: Bundle) => b.cache.getWorkUnits() !== undefined;
+const noWorkUnits = (b: Bundle) => b.cache.getWorkUnits() === undefined;
const summarizeBundles = (w: Assignments) =>
[
- w.moduleCount ? `${w.moduleCount} known modules` : '',
+ w.workUnits ? `${w.workUnits} work units` : '',
w.newBundles ? `${w.newBundles} new bundles` : '',
]
.filter(Boolean)
@@ -42,15 +42,15 @@ const assertReturnVal = (workers: Assignments[]) => {
expect(workers).toBeInstanceOf(Array);
for (const worker of workers) {
expect(worker).toEqual({
- moduleCount: expect.any(Number),
+ workUnits: expect.any(Number),
newBundles: expect.any(Number),
bundles: expect.any(Array),
});
- expect(worker.bundles.filter(noModuleCount).length).toBe(worker.newBundles);
+ expect(worker.bundles.filter(noWorkUnits).length).toBe(worker.newBundles);
expect(
- worker.bundles.filter(hasModuleCount).reduce((sum, b) => sum + b.cache.getModuleCount()!, 0)
- ).toBe(worker.moduleCount);
+ worker.bundles.filter(hasWorkUnits).reduce((sum, b) => sum + b.cache.getWorkUnits()!, 0)
+ ).toBe(worker.workUnits);
}
};
@@ -76,7 +76,7 @@ const getBundles = ({
for (let i = 1; i <= withCounts; i++) {
const id = `foo${i}`;
const bundle = testBundle(id);
- bundle.cache.set({ moduleCount: i % 5 === 0 ? i * 10 : i });
+ bundle.cache.set({ workUnits: i % 5 === 0 ? i * 10 : i });
bundles.push(bundle);
}
@@ -95,8 +95,8 @@ it('creates less workers if maxWorkersCount is larger than bundle count', () =>
expect(workers.length).toBe(2);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (1 known modules) => foo1",
- "worker 1 (2 known modules) => foo2",
+ "worker 0 (1 work units) => foo1",
+ "worker 1 (2 work units) => foo2",
]
`);
});
@@ -121,10 +121,10 @@ it('distributes bundles without module counts evenly after assigning modules wit
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (78 known modules, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1",
- "worker 1 (78 known modules, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0",
- "worker 2 (100 known modules, 2 new bundles) => foo10,bar7,bar3",
- "worker 3 (150 known modules, 2 new bundles) => foo15,bar6,bar2",
+ "worker 0 (78 work units, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1",
+ "worker 1 (78 work units, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0",
+ "worker 2 (100 work units, 2 new bundles) => foo10,bar7,bar3",
+ "worker 3 (150 work units, 2 new bundles) => foo15,bar6,bar2",
]
`);
});
@@ -135,8 +135,8 @@ it('distributes 2 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (1 known modules) => foo1",
- "worker 1 (2 known modules) => foo2",
+ "worker 0 (1 work units) => foo1",
+ "worker 1 (2 work units) => foo2",
]
`);
});
@@ -147,10 +147,10 @@ it('distributes 5 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (3 known modules) => foo2,foo1",
- "worker 1 (3 known modules) => foo3",
- "worker 2 (4 known modules) => foo4",
- "worker 3 (50 known modules) => foo5",
+ "worker 0 (3 work units) => foo2,foo1",
+ "worker 1 (3 work units) => foo3",
+ "worker 2 (4 work units) => foo4",
+ "worker 3 (50 work units) => foo5",
]
`);
});
@@ -161,10 +161,10 @@ it('distributes 10 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (20 known modules) => foo9,foo6,foo4,foo1",
- "worker 1 (20 known modules) => foo8,foo7,foo3,foo2",
- "worker 2 (50 known modules) => foo5",
- "worker 3 (100 known modules) => foo10",
+ "worker 0 (20 work units) => foo9,foo6,foo4,foo1",
+ "worker 1 (20 work units) => foo8,foo7,foo3,foo2",
+ "worker 2 (50 work units) => foo5",
+ "worker 3 (100 work units) => foo10",
]
`);
});
@@ -175,10 +175,10 @@ it('distributes 15 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (70 known modules) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1",
- "worker 1 (70 known modules) => foo5,foo8,foo7,foo3,foo2",
- "worker 2 (100 known modules) => foo10",
- "worker 3 (150 known modules) => foo15",
+ "worker 0 (70 work units) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1",
+ "worker 1 (70 work units) => foo5,foo8,foo7,foo3,foo2",
+ "worker 2 (100 work units) => foo10",
+ "worker 3 (150 work units) => foo15",
]
`);
});
@@ -189,10 +189,10 @@ it('distributes 20 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (153 known modules) => foo15,foo3",
- "worker 1 (153 known modules) => foo10,foo16,foo13,foo11,foo7,foo6",
- "worker 2 (154 known modules) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1",
- "worker 3 (200 known modules) => foo20",
+ "worker 0 (153 work units) => foo15,foo3",
+ "worker 1 (153 work units) => foo10,foo16,foo13,foo11,foo7,foo6",
+ "worker 2 (154 work units) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1",
+ "worker 3 (200 work units) => foo20",
]
`);
});
@@ -203,10 +203,10 @@ it('distributes 25 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (250 known modules) => foo20,foo17,foo13,foo9,foo8,foo2,foo1",
- "worker 1 (250 known modules) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3",
- "worker 2 (250 known modules) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4",
- "worker 3 (250 known modules) => foo25",
+ "worker 0 (250 work units) => foo20,foo17,foo13,foo9,foo8,foo2,foo1",
+ "worker 1 (250 work units) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3",
+ "worker 2 (250 work units) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4",
+ "worker 3 (250 work units) => foo25",
]
`);
});
@@ -217,10 +217,10 @@ it('distributes 30 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (352 known modules) => foo30,foo22,foo14,foo11,foo4,foo1",
- "worker 1 (352 known modules) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6",
- "worker 2 (353 known modules) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2",
- "worker 3 (353 known modules) => foo25,foo27,foo26,foo18,foo17,foo8,foo7",
+ "worker 0 (352 work units) => foo30,foo22,foo14,foo11,foo4,foo1",
+ "worker 1 (352 work units) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6",
+ "worker 2 (353 work units) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2",
+ "worker 3 (353 work units) => foo25,foo27,foo26,foo18,foo17,foo8,foo7",
]
`);
});
diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
index e1bcb22230bf9..44a3b21c5fd47 100644
--- a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
+++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
@@ -20,19 +20,18 @@
import { Bundle, descending, ascending } from '../common';
// helper types used inside getWorkerConfigs so we don't have
-// to calculate moduleCounts over and over
-
+// to calculate workUnits over and over
export interface Assignments {
- moduleCount: number;
+ workUnits: number;
newBundles: number;
bundles: Bundle[];
}
/** assign a wrapped bundle to a worker */
const assignBundle = (worker: Assignments, bundle: Bundle) => {
- const moduleCount = bundle.cache.getModuleCount();
- if (moduleCount !== undefined) {
- worker.moduleCount += moduleCount;
+ const workUnits = bundle.cache.getWorkUnits();
+ if (workUnits !== undefined) {
+ worker.workUnits += workUnits;
} else {
worker.newBundles += 1;
}
@@ -59,7 +58,7 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
const workers: Assignments[] = [];
for (let i = 0; i < workerCount; i++) {
workers.push({
- moduleCount: 0,
+ workUnits: 0,
newBundles: 0,
bundles: [],
});
@@ -67,18 +66,18 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
/**
* separate the bundles which do and don't have module
- * counts and sort them by [moduleCount, id]
+ * counts and sort them by [workUnits, id]
*/
const bundlesWithCountsDesc = bundles
- .filter((b) => b.cache.getModuleCount() !== undefined)
+ .filter((b) => b.cache.getWorkUnits() !== undefined)
.sort(
descending(
- (b) => b.cache.getModuleCount(),
+ (b) => b.cache.getWorkUnits(),
(b) => b.id
)
);
const bundlesWithoutModuleCounts = bundles
- .filter((b) => b.cache.getModuleCount() === undefined)
+ .filter((b) => b.cache.getWorkUnits() === undefined)
.sort(descending((b) => b.id));
/**
@@ -87,9 +86,9 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
* with module counts are assigned
*/
while (bundlesWithCountsDesc.length) {
- const [smallestWorker, nextSmallestWorker] = workers.sort(ascending((w) => w.moduleCount));
+ const [smallestWorker, nextSmallestWorker] = workers.sort(ascending((w) => w.workUnits));
- while (!nextSmallestWorker || smallestWorker.moduleCount <= nextSmallestWorker.moduleCount) {
+ while (!nextSmallestWorker || smallestWorker.workUnits <= nextSmallestWorker.workUnits) {
const bundle = bundlesWithCountsDesc.shift();
if (!bundle) {
@@ -104,7 +103,7 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
* assign bundles without module counts to workers round-robin
* starting with the smallest workers
*/
- workers.sort(ascending((w) => w.moduleCount));
+ workers.sort(ascending((w) => w.workUnits));
while (bundlesWithoutModuleCounts.length) {
for (const worker of workers) {
const bundle = bundlesWithoutModuleCounts.shift();
diff --git a/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts b/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts
new file mode 100644
index 0000000000000..3e848fe616b49
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { filterById, HasId } from './filter_by_id';
+
+const bundles: HasId[] = [
+ { id: 'foo' },
+ { id: 'bar' },
+ { id: 'abc' },
+ { id: 'abcd' },
+ { id: 'abcde' },
+ { id: 'example_a' },
+];
+
+const print = (result: HasId[]) =>
+ result
+ .map((b) => b.id)
+ .sort((a, b) => a.localeCompare(b))
+ .join(', ');
+
+it('[] matches everything', () => {
+ expect(print(filterById([], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('* matches everything', () => {
+ expect(print(filterById(['*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('combines mutliple filters to select any bundle which is matched', () => {
+ expect(print(filterById(['foo', 'bar'], bundles))).toMatchInlineSnapshot(`"bar, foo"`);
+ expect(print(filterById(['bar', 'abc*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar"`
+ );
+});
+
+it('matches everything if any filter is *', () => {
+ expect(print(filterById(['*', '!abc*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('only matches bundles which are matched by an entire single filter', () => {
+ expect(print(filterById(['*,!abc*'], bundles))).toMatchInlineSnapshot(`"bar, example_a, foo"`);
+});
+
+it('handles purely positive filters', () => {
+ expect(print(filterById(['abc*'], bundles))).toMatchInlineSnapshot(`"abc, abcd, abcde"`);
+});
+
+it('handles purely negative filters', () => {
+ expect(print(filterById(['!abc*'], bundles))).toMatchInlineSnapshot(`"bar, example_a, foo"`);
+});
diff --git a/packages/kbn-optimizer/src/optimizer/filter_by_id.ts b/packages/kbn-optimizer/src/optimizer/filter_by_id.ts
new file mode 100644
index 0000000000000..ccf61a9efc880
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/filter_by_id.ts
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+export interface HasId {
+ id: string;
+}
+
+function parseFilter(filter: string) {
+ const positive: RegExp[] = [];
+ const negative: RegExp[] = [];
+
+ for (const segment of filter.split(',')) {
+ let trimmed = segment.trim();
+ let list = positive;
+
+ if (trimmed.startsWith('!')) {
+ trimmed = trimmed.slice(1);
+ list = negative;
+ }
+
+ list.push(new RegExp(`^${trimmed.split('*').join('.*')}$`));
+ }
+
+ return (bundle: HasId) =>
+ (!positive.length || positive.some((p) => p.test(bundle.id))) &&
+ (!negative.length || !negative.some((p) => p.test(bundle.id)));
+}
+
+export function filterById(filterStrings: string[], bundles: T[]) {
+ const filters = filterStrings.map(parseFilter);
+ return bundles.filter((b) => !filters.length || filters.some((f) => f(b)));
+}
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 bbd3ddc11f448..a70cfc759dd55 100644
--- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts
@@ -32,18 +32,21 @@ it('returns a bundle for core and each plugin', () => {
id: 'foo',
isUiPlugin: true,
extraPublicDirs: [],
+ manifestPath: '/repo/plugins/foo/kibana.json',
},
{
directory: '/repo/plugins/bar',
id: 'bar',
isUiPlugin: false,
extraPublicDirs: [],
+ manifestPath: '/repo/plugins/bar/kibana.json',
},
{
directory: '/outside/of/repo/plugins/baz',
id: 'baz',
isUiPlugin: true,
extraPublicDirs: [],
+ manifestPath: '/outside/of/repo/plugins/baz/kibana.json',
},
],
'/repo'
@@ -53,6 +56,7 @@ it('returns a bundle for core and each plugin', () => {
Object {
"contextDir": /plugins/foo,
"id": "foo",
+ "manifestPath": /plugins/foo/kibana.json,
"outputDir": /plugins/foo/target/public,
"publicDirNames": Array [
"public",
@@ -63,6 +67,7 @@ it('returns a bundle for core and each plugin', () => {
Object {
"contextDir": "/outside/of/repo/plugins/baz",
"id": "baz",
+ "manifestPath": "/outside/of/repo/plugins/baz/kibana.json",
"outputDir": "/outside/of/repo/plugins/baz/target/public",
"publicDirNames": Array [
"public",
diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
index 2635289088725..04ab992addeec 100644
--- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
+++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
@@ -35,6 +35,7 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri
sourceRoot: repoRoot,
contextDir: p.directory,
outputDir: Path.resolve(p.directory, 'target/public'),
+ manifestPath: p.manifestPath,
})
);
}
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 f7b457ca42c6d..06fffc953f58b 100644
--- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts
@@ -40,24 +40,28 @@ it('parses kibana.json files of plugins found in pluginDirs', () => {
"extraPublicDirs": Array [],
"id": "bar",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo,
"extraPublicDirs": Array [],
"id": "foo",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz,
"extraPublicDirs": Array [],
"id": "baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz,
"extraPublicDirs": Array [],
"id": "test_baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json,
},
]
`);
diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
index 83637691004f4..b489c53be47b9 100644
--- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
+++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
@@ -24,6 +24,7 @@ import loadJsonFile from 'load-json-file';
export interface KibanaPlatformPlugin {
readonly directory: string;
+ readonly manifestPath: string;
readonly id: string;
readonly isUiPlugin: boolean;
readonly extraPublicDirs: string[];
@@ -92,6 +93,7 @@ function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin {
return {
directory: Path.dirname(manifestPath),
+ manifestPath,
id: manifest.id,
isUiPlugin: !!manifest.ui,
extraPublicDirs: extraPublicDirs || [],
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
index 5b46d67479fd5..f97646e2bbbd3 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
@@ -21,6 +21,7 @@ jest.mock('./assign_bundles_to_workers.ts');
jest.mock('./kibana_platform_plugins.ts');
jest.mock('./get_plugin_bundles.ts');
jest.mock('../common/theme_tags.ts');
+jest.mock('./filter_by_id.ts');
import Path from 'path';
import Os from 'os';
@@ -113,6 +114,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -139,6 +141,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -165,6 +168,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -193,6 +197,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -218,6 +223,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -243,6 +249,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -265,6 +272,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -287,6 +295,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -310,6 +319,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -333,6 +343,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -358,6 +369,7 @@ describe('OptimizerConfig::create()', () => {
const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts')
.findKibanaPlatformPlugins;
const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles;
+ const filterById: jest.Mock = jest.requireMock('./filter_by_id.ts').filterById;
beforeEach(() => {
if ('mock' in OptimizerConfig.parseOptions) {
@@ -370,6 +382,7 @@ describe('OptimizerConfig::create()', () => {
]);
findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins'));
getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]);
+ filterById.mockReturnValue(Symbol('filtered bundles'));
jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({
cache: Symbol('parsed cache'),
@@ -382,6 +395,7 @@ describe('OptimizerConfig::create()', () => {
themeTags: Symbol('theme tags'),
inspectWorkers: Symbol('parsed inspect workers'),
profileWebpack: Symbol('parsed profile webpack'),
+ filters: [],
}));
});
@@ -392,10 +406,7 @@ describe('OptimizerConfig::create()', () => {
expect(config).toMatchInlineSnapshot(`
OptimizerConfig {
- "bundles": Array [
- Symbol(bundle1),
- Symbol(bundle2),
- ],
+ "bundles": Symbol(filtered bundles),
"cache": Symbol(parsed cache),
"dist": Symbol(parsed dist),
"inspectWorkers": Symbol(parsed inspect workers),
@@ -431,6 +442,32 @@ describe('OptimizerConfig::create()', () => {
}
`);
+ expect(filterById.mock).toMatchInlineSnapshot(`
+ Object {
+ "calls": Array [
+ Array [
+ Array [],
+ Array [
+ Symbol(bundle1),
+ Symbol(bundle2),
+ ],
+ ],
+ ],
+ "instances": Array [
+ [Window],
+ ],
+ "invocationCallOrder": Array [
+ 23,
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Symbol(filtered bundles),
+ },
+ ],
+ }
+ `);
+
expect(getPluginBundles.mock).toMatchInlineSnapshot(`
Object {
"calls": Array [
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
index 7757004139d0d..0e588ab36238b 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
@@ -31,6 +31,7 @@ import {
import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins';
import { getPluginBundles } from './get_plugin_bundles';
+import { filterById } from './filter_by_id';
function pickMaxWorkerCount(dist: boolean) {
// don't break if cpus() returns nothing, or an empty array
@@ -77,6 +78,18 @@ interface Options {
pluginScanDirs?: string[];
/** absolute paths that should be added to the default scan dirs */
extraPluginScanDirs?: string[];
+ /**
+ * array of comma separated patterns that will be matched against bundle ids.
+ * bundles will only be built if they match one of the specified patterns.
+ * `*` can exist anywhere in each pattern and will match anything, `!` inverts the pattern
+ *
+ * examples:
+ * --filter foo --filter bar # [foo, bar], excludes [foobar]
+ * --filter foo,bar # [foo, bar], excludes [foobar]
+ * --filter foo* # [foo, foobar], excludes [bar]
+ * --filter f*r # [foobar], excludes [foo, bar]
+ */
+ filter?: string[];
/** flag that causes the core bundle to be built along with plugins */
includeCoreBundle?: boolean;
@@ -103,6 +116,7 @@ interface ParsedOptions {
dist: boolean;
pluginPaths: string[];
pluginScanDirs: string[];
+ filters: string[];
inspectWorkers: boolean;
includeCoreBundle: boolean;
themeTags: ThemeTags;
@@ -118,6 +132,7 @@ export class OptimizerConfig {
const inspectWorkers = !!options.inspectWorkers;
const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE;
const includeCoreBundle = !!options.includeCoreBundle;
+ const filters = options.filter || [];
const repoRoot = options.repoRoot;
if (!Path.isAbsolute(repoRoot)) {
@@ -172,6 +187,7 @@ export class OptimizerConfig {
cache,
pluginScanDirs,
pluginPaths,
+ filters,
inspectWorkers,
includeCoreBundle,
themeTags,
@@ -198,7 +214,7 @@ export class OptimizerConfig {
];
return new OptimizerConfig(
- bundles,
+ filterById(options.filters, bundles),
options.cache,
options.watch,
options.inspectWorkers,
diff --git a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
index cde25564cf528..563b4ecb4bc37 100644
--- a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
@@ -10,6 +10,7 @@
// @ts-ignore not typed by @types/webpack
import Module from 'webpack/lib/Module';
+import { BundleRef } from '../common';
export class BundleRefModule extends Module {
public built = false;
@@ -17,12 +18,12 @@ export class BundleRefModule extends Module {
public buildInfo?: any;
public exportsArgument = '__webpack_exports__';
- constructor(public readonly exportId: string) {
+ constructor(public readonly ref: BundleRef) {
super('kbn/bundleRef', null);
}
libIdent() {
- return this.exportId;
+ return this.ref.exportId;
}
chunkCondition(chunk: any) {
@@ -30,7 +31,7 @@ export class BundleRefModule extends Module {
}
identifier() {
- return '@kbn/bundleRef ' + JSON.stringify(this.exportId);
+ return '@kbn/bundleRef ' + JSON.stringify(this.ref.exportId);
}
readableIdentifier() {
@@ -51,7 +52,7 @@ export class BundleRefModule extends Module {
source() {
return `
__webpack_require__.r(__webpack_exports__);
- var ns = __kbnBundles__.get('${this.exportId}');
+ var ns = __kbnBundles__.get('${this.ref.exportId}');
Object.defineProperties(__webpack_exports__, Object.getOwnPropertyDescriptors(ns))
`;
}
diff --git a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
index 9c4d5ed7f8a98..5396d11726f7a 100644
--- a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
@@ -44,6 +44,7 @@ export class BundleRefsPlugin {
private readonly resolvedRefEntryCache = new Map>();
private readonly resolvedRequestCache = new Map>();
private readonly ignorePrefix = Path.resolve(this.bundle.contextDir) + Path.sep;
+ private allowedBundleIds = new Set();
constructor(private readonly bundle: Bundle, private readonly bundleRefs: BundleRefs) {}
@@ -81,6 +82,45 @@ export class BundleRefsPlugin {
}
);
});
+
+ compiler.hooks.compilation.tap('BundleRefsPlugin/getRequiredBundles', (compilation) => {
+ this.allowedBundleIds.clear();
+
+ const manifestPath = this.bundle.manifestPath;
+ if (!manifestPath) {
+ return;
+ }
+
+ const deps = this.bundle.readBundleDeps();
+ for (const ref of this.bundleRefs.forBundleIds([...deps.explicit, ...deps.implicit])) {
+ this.allowedBundleIds.add(ref.bundleId);
+ }
+
+ compilation.hooks.additionalAssets.tap('BundleRefsPlugin/watchManifest', () => {
+ compilation.fileDependencies.add(manifestPath);
+ });
+
+ compilation.hooks.finishModules.tapPromise(
+ 'BundleRefsPlugin/finishModules',
+ async (modules) => {
+ const usedBundleIds = (modules as any[])
+ .filter((m: any): m is BundleRefModule => m instanceof BundleRefModule)
+ .map((m) => m.ref.bundleId);
+
+ const unusedBundleIds = deps.explicit
+ .filter((id) => !usedBundleIds.includes(id))
+ .join(', ');
+
+ if (unusedBundleIds) {
+ const error = new Error(
+ `Bundle for [${this.bundle.id}] lists [${unusedBundleIds}] as a required bundle, but does not use it. Please remove it.`
+ );
+ (error as any).file = manifestPath;
+ compilation.errors.push(error);
+ }
+ }
+ );
+ });
}
private cachedResolveRefEntry(ref: BundleRef) {
@@ -170,21 +210,29 @@ export class BundleRefsPlugin {
return;
}
- const eligibleRefs = this.bundleRefs.filterByContextPrefix(this.bundle, resolved);
- if (!eligibleRefs.length) {
+ const possibleRefs = this.bundleRefs.filterByContextPrefix(this.bundle, resolved);
+ if (!possibleRefs.length) {
// import doesn't match a bundle context
return;
}
- for (const ref of eligibleRefs) {
+ for (const ref of possibleRefs) {
const resolvedEntry = await this.cachedResolveRefEntry(ref);
- if (resolved === resolvedEntry) {
- return new BundleRefModule(ref.exportId);
+ if (resolved !== resolvedEntry) {
+ continue;
}
+
+ if (!this.allowedBundleIds.has(ref.bundleId)) {
+ throw new Error(
+ `import [${request}] references a public export of the [${ref.bundleId}] bundle, but that bundle is not in the "requiredPlugins" or "requiredBundles" list in the plugin manifest [${this.bundle.manifestPath}]`
+ );
+ }
+
+ return new BundleRefModule(ref);
}
- const bundleId = Array.from(new Set(eligibleRefs.map((r) => r.bundleId))).join(', ');
- const publicDir = eligibleRefs.map((r) => r.entry).join(', ');
+ const bundleId = Array.from(new Set(possibleRefs.map((r) => r.bundleId))).join(', ');
+ const publicDir = possibleRefs.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}]`
);
diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts
index ca7673748bde9..c7be943d65a48 100644
--- a/packages/kbn-optimizer/src/worker/run_compilers.ts
+++ b/packages/kbn-optimizer/src/worker/run_compilers.ts
@@ -50,6 +50,15 @@ import {
const PLUGIN_NAME = '@kbn/optimizer';
+/**
+ * sass-loader creates about a 40% overhead on the overall optimizer runtime, and
+ * so this constant is used to indicate to assignBundlesToWorkers() that there is
+ * extra work done in a bundle that has a lot of scss imports. The value is
+ * arbitrary and just intended to weigh the bundles so that they are distributed
+ * across mulitple workers on machines with lots of cores.
+ */
+const EXTRA_SCSS_WORK_UNITS = 100;
+
/**
* Create an Observable for a specific child compiler + bundle
*/
@@ -102,6 +111,11 @@ const observeCompiler = (
const bundleRefExportIds: string[] = [];
const referencedFiles = new Set();
let normalModuleCount = 0;
+ let workUnits = stats.compilation.fileDependencies.size;
+
+ if (bundle.manifestPath) {
+ referencedFiles.add(bundle.manifestPath);
+ }
for (const module of stats.compilation.modules) {
if (isNormalModule(module)) {
@@ -111,6 +125,15 @@ const observeCompiler = (
if (!parsedPath.dirs.includes('node_modules')) {
referencedFiles.add(path);
+
+ if (path.endsWith('.scss')) {
+ workUnits += EXTRA_SCSS_WORK_UNITS;
+
+ for (const depPath of module.buildInfo.fileDependencies) {
+ referencedFiles.add(depPath);
+ }
+ }
+
continue;
}
@@ -127,7 +150,7 @@ const observeCompiler = (
}
if (module instanceof BundleRefModule) {
- bundleRefExportIds.push(module.exportId);
+ bundleRefExportIds.push(module.ref.exportId);
continue;
}
@@ -158,6 +181,7 @@ const observeCompiler = (
optimizerCacheKey: workerConfig.optimizerCacheKey,
cacheKey: bundle.createCacheKey(files, mtimes),
moduleCount: normalModuleCount,
+ workUnits,
files,
});
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 6ea4a621f92f6..8398d1c081da6 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@elastic/charts": "19.8.1",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
"@kbn/monaco": "1.0.0",
diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts
index 6db6199b391e1..f193f33e6f47e 100644
--- a/src/cli/cluster/cluster_manager.ts
+++ b/src/cli/cluster/cluster_manager.ts
@@ -261,7 +261,7 @@ export class ClusterManager {
/debug\.log$/,
...pluginInternalDirsIgnore,
fromRoot('src/legacy/server/sass/__tmp__'),
- fromRoot('x-pack/plugins/reporting/.chromium'),
+ fromRoot('x-pack/plugins/reporting/chromium'),
fromRoot('x-pack/plugins/security_solution/cypress'),
fromRoot('x-pack/plugins/apm/e2e'),
fromRoot('x-pack/plugins/apm/scripts'),
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 1b894bc400f08..d29120e6ee9ac 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Breadcrumb as EuiBreadcrumb, IconType } from '@elastic/eui';
+import { EuiBreadcrumb, IconType } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs';
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
index 1cfded4dc7b8f..9ecbc055e3320 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
@@ -372,12 +372,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
handler={[Function]}
/>
}
/>
@@ -3908,16 +3909,9 @@ exports[`CollapsibleNav renders the default nav 2`] = `
handler={[Function]}
/>
-
- }
- />
-
+ />
@@ -6914,7 +6914,7 @@ exports[`Header renders 3`] = `
>
@@ -12347,7 +12347,7 @@ exports[`Header renders 4`] = `
>
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
index fdaa17c279a10..5080b23e99c25 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
@@ -13,29 +13,14 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] =
exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 2`] = `
Array [
-
-
- First
-
- ,
-
First
- ,
+ ,
diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts
index 3f77161f8c34d..7331157951dec 100644
--- a/src/core/public/plugins/plugin.test.ts
+++ b/src/core/public/plugins/plugin.test.ts
@@ -32,6 +32,7 @@ function createManifest(
configPath: ['path'],
requiredPlugins: required,
optionalPlugins: optional,
+ requiredBundles: [],
} as DiscoveredPlugin;
}
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index 7dc5f3655fca0..28cbfce3fd651 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -73,6 +73,7 @@ function createManifest(
configPath: ['path'],
requiredPlugins: required,
optionalPlugins: optional,
+ requiredBundles: [],
};
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 40fc3f977006f..303d005197588 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -6,7 +6,6 @@
import { Action } from 'history';
import Boom from 'boom';
-import { Breadcrumb } from '@elastic/eui';
import { BulkIndexDocumentsParams } from 'elasticsearch';
import { CatAliasesParams } from 'elasticsearch';
import { CatAllocationParams } from 'elasticsearch';
@@ -37,6 +36,7 @@ import { DeleteDocumentByQueryParams } from 'elasticsearch';
import { DeleteDocumentParams } from 'elasticsearch';
import { DeleteScriptParams } from 'elasticsearch';
import { DeleteTemplateParams } from 'elasticsearch';
+import { EuiBreadcrumb } from '@elastic/eui';
import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
@@ -334,7 +334,7 @@ export interface ChromeBrand {
}
// @public (undocumented)
-export type ChromeBreadcrumb = Breadcrumb;
+export type ChromeBreadcrumb = EuiBreadcrumb;
// @public
export interface ChromeDocTitle {
diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss
index 1f9e35e62ddcc..211e9c03beea5 100644
--- a/src/core/public/rendering/_base.scss
+++ b/src/core/public/rendering/_base.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index ae4cf612e92ce..f8f04c59766b3 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -109,6 +109,7 @@ beforeEach(() => {
[
'plugin-id',
{
+ requiredBundles: [],
publicTargetDir: 'path/to/target/public',
publicAssetsDir: '/plugins/name/assets/',
},
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
index 5ffdef88104c8..64d1256be2f30 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
@@ -302,6 +302,7 @@ test('set defaults for all missing optional fields', async () => {
kibanaVersion: '7.0.0',
optionalPlugins: [],
requiredPlugins: [],
+ requiredBundles: [],
server: true,
ui: false,
});
@@ -331,6 +332,7 @@ test('return all set optional fields as they are in manifest', async () => {
version: 'some-version',
kibanaVersion: '7.0.0',
optionalPlugins: ['some-optional-plugin'],
+ requiredBundles: [],
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
server: false,
ui: true,
@@ -361,6 +363,7 @@ test('return manifest when plugin expected Kibana version matches actual version
kibanaVersion: '7.0.0-alpha2',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
+ requiredBundles: [],
server: true,
ui: false,
});
@@ -390,6 +393,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async ()
kibanaVersion: 'kibana',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
+ requiredBundles: [],
server: true,
ui: true,
});
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
index f2c3a29eca0ac..0d33e266c37db 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
@@ -58,6 +58,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
ui: true,
server: true,
extraPublicDirs: true,
+ requiredBundles: true,
};
return new Set(Object.keys(manifestFields));
@@ -191,6 +192,7 @@ export async function parseManifest(
configPath: manifest.configPath || snakeCase(manifest.id),
requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [],
optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [],
+ requiredBundles: Array.isArray(manifest.requiredBundles) ? manifest.requiredBundles : [],
ui: includesUiPlugin,
server: includesServerPlugin,
extraPublicDirs: manifest.extraPublicDirs,
diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts
index e676c789449ca..49c129d0ae67d 100644
--- a/src/core/server/plugins/integration_tests/plugins_service.test.ts
+++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts
@@ -43,6 +43,7 @@ describe('PluginsService', () => {
disabled = false,
version = 'some-version',
requiredPlugins = [],
+ requiredBundles = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
configPath = [path],
@@ -53,6 +54,7 @@ describe('PluginsService', () => {
disabled?: boolean;
version?: string;
requiredPlugins?: string[];
+ requiredBundles?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
configPath?: ConfigPath;
@@ -68,6 +70,7 @@ describe('PluginsService', () => {
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
requiredPlugins,
+ requiredBundles,
optionalPlugins,
server,
ui,
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index ec0a3986b4877..4f26686e1f5e0 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -54,6 +54,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
kibanaVersion: '7.0.0',
requiredPlugins: ['some-required-dep'],
optionalPlugins: ['some-optional-dep'],
+ requiredBundles: [],
server: true,
ui: true,
...manifestProps,
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index 2e5881c651843..8bd9840855654 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -53,6 +53,7 @@ export class PluginWrapper<
public readonly configPath: PluginManifest['configPath'];
public readonly requiredPlugins: PluginManifest['requiredPlugins'];
public readonly optionalPlugins: PluginManifest['optionalPlugins'];
+ public readonly requiredBundles: PluginManifest['requiredBundles'];
public readonly includesServerPlugin: PluginManifest['server'];
public readonly includesUiPlugin: PluginManifest['ui'];
@@ -81,6 +82,7 @@ export class PluginWrapper<
this.configPath = params.manifest.configPath;
this.requiredPlugins = params.manifest.requiredPlugins;
this.optionalPlugins = params.manifest.optionalPlugins;
+ this.requiredBundles = params.manifest.requiredBundles;
this.includesServerPlugin = params.manifest.server;
this.includesUiPlugin = params.manifest.ui;
}
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 69b354661abc9..ebd068caadfb9 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -43,6 +43,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
configPath: 'path',
kibanaVersion: '7.0.0',
requiredPlugins: ['some-required-dep'],
+ requiredBundles: [],
optionalPlugins: ['some-optional-dep'],
server: true,
ui: true,
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 46fd2b00c2304..aa77335991e2c 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -64,6 +64,7 @@ const createPlugin = (
disabled = false,
version = 'some-version',
requiredPlugins = [],
+ requiredBundles = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
configPath = [path],
@@ -74,6 +75,7 @@ const createPlugin = (
disabled?: boolean;
version?: string;
requiredPlugins?: string[];
+ requiredBundles?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
configPath?: ConfigPath;
@@ -89,6 +91,7 @@ const createPlugin = (
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
requiredPlugins,
+ requiredBundles,
optionalPlugins,
server,
ui,
@@ -460,6 +463,7 @@ describe('PluginsService', () => {
id: plugin.name,
configPath: plugin.manifest.configPath,
requiredPlugins: [],
+ requiredBundles: [],
optionalPlugins: [],
},
];
@@ -563,10 +567,12 @@ describe('PluginsService', () => {
"plugin-1" => Object {
"publicAssetsDir": /path-1/public/assets,
"publicTargetDir": /path-1/target/public,
+ "requiredBundles": Array [],
},
"plugin-2" => Object {
"publicAssetsDir": /path-2/public/assets,
"publicTargetDir": /path-2/target/public,
+ "requiredBundles": Array [],
},
}
`);
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 5d1261e697bc0..06de48a215881 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -228,6 +228,7 @@ export class PluginsService implements CoreService
uiPluginNames.includes(p)
),
+ requiredBundles: plugin.manifest.requiredBundles,
},
];
})
diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts
index 9e86ee22c607b..9695c9171a771 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -136,6 +136,18 @@ export interface PluginManifest {
*/
readonly requiredPlugins: readonly PluginName[];
+ /**
+ * List of plugin ids that this plugin's UI code imports modules from that are
+ * not in `requiredPlugins`.
+ *
+ * @remarks
+ * The plugins listed here will be loaded in the browser, even if the plugin is
+ * disabled. Required by `@kbn/optimizer` to support cross-plugin imports.
+ * "core" and plugins already listed in `requiredPlugins` do not need to be
+ * duplicated here.
+ */
+ readonly requiredBundles: readonly string[];
+
/**
* 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
@@ -191,12 +203,28 @@ export interface DiscoveredPlugin {
* not required for this plugin to work properly.
*/
readonly optionalPlugins: readonly PluginName[];
+
+ /**
+ * List of plugin ids that this plugin's UI code imports modules from that are
+ * not in `requiredPlugins`.
+ *
+ * @remarks
+ * The plugins listed here will be loaded in the browser, even if the plugin is
+ * disabled. Required by `@kbn/optimizer` to support cross-plugin imports.
+ * "core" and plugins already listed in `requiredPlugins` do not need to be
+ * duplicated here.
+ */
+ readonly requiredBundles: readonly PluginName[];
}
/**
* @internal
*/
export interface InternalPluginInfo {
+ /**
+ * Bundles that must be loaded for this plugoin
+ */
+ readonly requiredBundles: readonly string[];
/**
* Path to the target/public directory of the plugin which should be
* served
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 95912c3af63e5..3d3e1905577d9 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -637,6 +637,7 @@ export interface DiscoveredPlugin {
readonly configPath: ConfigPath;
readonly id: PluginName;
readonly optionalPlugins: readonly PluginName[];
+ readonly requiredBundles: readonly PluginName[];
readonly requiredPlugins: readonly PluginName[];
}
@@ -1684,6 +1685,7 @@ export interface PluginManifest {
readonly id: PluginName;
readonly kibanaVersion: string;
readonly optionalPlugins: readonly PluginName[];
+ readonly requiredBundles: readonly string[];
readonly requiredPlugins: readonly PluginName[];
readonly server: boolean;
readonly ui: boolean;
@@ -2706,8 +2708,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: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
+// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
```
diff --git a/src/dev/build/README.md b/src/dev/build/README.md
index 3b579033fabe1..ed8750f6fee56 100644
--- a/src/dev/build/README.md
+++ b/src/dev/build/README.md
@@ -24,7 +24,7 @@ The majority of this logic is extracted from the grunt build that has existed fo
**Config**: [lib/config.js] defines the config used to execute tasks. It is mostly used to determine absolute paths to specific locations, and to get access to the Platforms.
-**Platform**: [lib/platform.js] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getLinux/Windows/MacPlatform()` to get a specific platform.
+**Platform**: [lib/platform.js] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getPlatform` to get a specific platform and architecture.
**Log**: We uses the `ToolingLog` defined in [../tooling_log/tooling_log.js]
diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js
index 2ea71fa2c1d33..22a348b78dc0a 100644
--- a/src/dev/build/build_distributables.js
+++ b/src/dev/build/build_distributables.js
@@ -20,16 +20,16 @@
import { getConfig, createRunner } from './lib';
import {
+ BuildKibanaPlatformPluginsTask,
BuildPackagesTask,
CleanClientModulesOnDLLTask,
CleanEmptyFoldersTask,
CleanExtraBinScriptsTask,
- CleanExtraBrowsersTask,
CleanExtraFilesFromModulesTask,
- CleanPackagesTask,
- CleanTypescriptTask,
CleanNodeBuildsTask,
+ CleanPackagesTask,
CleanTask,
+ CleanTypescriptTask,
CopyBinScriptsTask,
CopySourceTask,
CreateArchivesSourcesTask,
@@ -44,20 +44,20 @@ import {
CreateRpmPackageTask,
DownloadNodeBuildsTask,
ExtractNodeBuildsTask,
+ InstallChromiumTask,
InstallDependenciesTask,
- BuildKibanaPlatformPluginsTask,
OptimizeBuildTask,
PatchNativeModulesTask,
+ PathLengthTask,
RemovePackageJsonDepsTask,
RemoveWorkspacesTask,
TranspileBabelTask,
TranspileScssTask,
UpdateLicenseFileTask,
+ UuidVerificationTask,
VerifyEnvTask,
VerifyExistingNodeBuildsTask,
- PathLengthTask,
WriteShaSumsTask,
- UuidVerificationTask,
} from './tasks';
export async function buildDistributables(options) {
@@ -134,12 +134,12 @@ export async function buildDistributables(options) {
/**
* copy generic build outputs into platform-specific build
- * directories and perform platform-specific steps
+ * directories and perform platform/architecture-specific steps
*/
await run(CreateArchivesSourcesTask);
await run(PatchNativeModulesTask);
+ await run(InstallChromiumTask);
await run(CleanExtraBinScriptsTask);
- await run(CleanExtraBrowsersTask);
await run(CleanNodeBuildsTask);
await run(PathLengthTask);
diff --git a/src/dev/build/lib/__tests__/config.js b/src/dev/build/lib/__tests__/config.js
index d2f408378da25..9544fc84dc6ff 100644
--- a/src/dev/build/lib/__tests__/config.js
+++ b/src/dev/build/lib/__tests__/config.js
@@ -72,15 +72,31 @@ describe('dev/build/lib/config', () => {
});
});
+ describe('#getPlatform()', () => {
+ it('throws error when platform does not exist', async () => {
+ const { config } = await setup();
+ const fn = () => config.getPlatform('foo', 'x64');
+
+ expect(fn).to.throwException(/Unable to find platform/);
+ });
+
+ it('throws error when architecture does not exist', async () => {
+ const { config } = await setup();
+ const fn = () => config.getPlatform('linux', 'foo');
+
+ expect(fn).to.throwException(/Unable to find platform/);
+ });
+ });
+
describe('#getTargetPlatforms()', () => {
it('returns an array of all platform objects', async () => {
const { config } = await setup();
expect(
config
.getTargetPlatforms()
- .map((p) => p.getName())
+ .map((p) => p.getNodeArch())
.sort()
- ).to.eql(['darwin', 'linux', 'windows']);
+ ).to.eql(['darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']);
});
it('returns just this platform when targetAllPlatforms = false', async () => {
@@ -99,9 +115,9 @@ describe('dev/build/lib/config', () => {
expect(
config
.getTargetPlatforms()
- .map((p) => p.getName())
+ .map((p) => p.getNodeArch())
.sort()
- ).to.eql(['darwin', 'linux', 'windows']);
+ ).to.eql(['darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']);
});
it('returns this platform and linux, when targetAllPlatforms = false', async () => {
@@ -111,39 +127,20 @@ describe('dev/build/lib/config', () => {
if (process.platform !== 'linux') {
expect(platforms).to.have.length(2);
expect(platforms[0]).to.be(config.getPlatformForThisOs());
- expect(platforms[1]).to.be(config.getLinuxPlatform());
+ expect(platforms[1]).to.be(config.getPlatform('linux', 'x64'));
} else {
expect(platforms).to.have.length(1);
- expect(platforms[0]).to.be(config.getLinuxPlatform());
+ expect(platforms[0]).to.be(config.getPlatform('linux', 'x64'));
}
});
});
- describe('#getLinuxPlatform()', () => {
- it('returns the linux platform', async () => {
- const { config } = await setup();
- expect(config.getLinuxPlatform().getName()).to.be('linux');
- });
- });
-
- describe('#getWindowsPlatform()', () => {
- it('returns the windows platform', async () => {
- const { config } = await setup();
- expect(config.getWindowsPlatform().getName()).to.be('windows');
- });
- });
-
- describe('#getMacPlatform()', () => {
- it('returns the mac platform', async () => {
- const { config } = await setup();
- expect(config.getMacPlatform().getName()).to.be('darwin');
- });
- });
-
describe('#getPlatformForThisOs()', () => {
it('returns the platform that matches the arch of this machine', async () => {
const { config } = await setup();
- expect(config.getPlatformForThisOs().getName()).to.be(process.platform);
+ const currentPlatform = config.getPlatformForThisOs();
+ expect(currentPlatform.getName()).to.be(process.platform);
+ expect(currentPlatform.getArchitecture()).to.be(process.arch);
});
});
diff --git a/src/dev/build/lib/__tests__/platform.js b/src/dev/build/lib/__tests__/platform.js
index 86ef1749feca9..a7bb5670ee412 100644
--- a/src/dev/build/lib/__tests__/platform.js
+++ b/src/dev/build/lib/__tests__/platform.js
@@ -30,37 +30,39 @@ describe('src/dev/build/lib/platform', () => {
describe('getNodeArch()', () => {
it('returns the node arch for the passed name', () => {
- expect(createPlatform('windows').getNodeArch()).to.be('windows-x64');
+ expect(createPlatform('win32', 'x64').getNodeArch()).to.be('win32-x64');
});
});
describe('getBuildName()', () => {
it('returns the build name for the passed name', () => {
- expect(createPlatform('windows').getBuildName()).to.be('windows-x86_64');
+ expect(createPlatform('linux', 'arm64', 'linux-aarch64').getBuildName()).to.be(
+ 'linux-aarch64'
+ );
});
});
describe('isWindows()', () => {
- it('returns true if name is windows', () => {
- expect(createPlatform('windows').isWindows()).to.be(true);
- expect(createPlatform('linux').isWindows()).to.be(false);
- expect(createPlatform('darwin').isWindows()).to.be(false);
+ it('returns true if name is win32', () => {
+ expect(createPlatform('win32', 'x64').isWindows()).to.be(true);
+ expect(createPlatform('linux', 'x64').isWindows()).to.be(false);
+ expect(createPlatform('darwin', 'x64').isWindows()).to.be(false);
});
});
describe('isLinux()', () => {
it('returns true if name is linux', () => {
- expect(createPlatform('windows').isLinux()).to.be(false);
- expect(createPlatform('linux').isLinux()).to.be(true);
- expect(createPlatform('darwin').isLinux()).to.be(false);
+ expect(createPlatform('win32', 'x64').isLinux()).to.be(false);
+ expect(createPlatform('linux', 'x64').isLinux()).to.be(true);
+ expect(createPlatform('darwin', 'x64').isLinux()).to.be(false);
});
});
describe('isMac()', () => {
it('returns true if name is darwin', () => {
- expect(createPlatform('windows').isMac()).to.be(false);
- expect(createPlatform('linux').isMac()).to.be(false);
- expect(createPlatform('darwin').isMac()).to.be(true);
+ expect(createPlatform('win32', 'x64').isMac()).to.be(false);
+ expect(createPlatform('linux', 'x64').isMac()).to.be(false);
+ expect(createPlatform('darwin', 'x64').isMac()).to.be(true);
});
});
});
diff --git a/src/dev/build/lib/config.js b/src/dev/build/lib/config.js
index cd762d9bb1f20..36621f1c2d4ac 100644
--- a/src/dev/build/lib/config.js
+++ b/src/dev/build/lib/config.js
@@ -18,7 +18,7 @@
*/
import { dirname, resolve, relative } from 'path';
-import { platform as getOsPlatform } from 'os';
+import os from 'os';
import { getVersionInfo } from './version_info';
import { createPlatform } from './platform';
@@ -29,7 +29,12 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
const repoRoot = dirname(pkgPath);
const nodeVersion = pkg.engines.node;
- const platforms = ['darwin', 'linux', 'windows'].map(createPlatform);
+ const platforms = [
+ createPlatform('linux', 'x64', 'linux-x86_64'),
+ createPlatform('linux', 'arm64', 'linux-aarch64'),
+ createPlatform('darwin', 'x64', 'darwin-x86_64'),
+ createPlatform('win32', 'x64', 'windows-x86_64'),
+ ];
const versionInfo = await getVersionInfo({
isRelease,
@@ -101,34 +106,22 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
}
if (process.platform === 'linux') {
- return [this.getLinuxPlatform()];
+ return [this.getPlatform('linux', 'x64')];
}
- return [this.getPlatformForThisOs(), this.getLinuxPlatform()];
+ return [this.getPlatformForThisOs(), this.getPlatform('linux', 'x64')];
}
- /**
- * Get the linux platform object
- * @return {Platform}
- */
- getLinuxPlatform() {
- return platforms.find((p) => p.isLinux());
- }
+ getPlatform(name, arch) {
+ const selected = platforms.find((p) => {
+ return name === p.getName() && arch === p.getArchitecture();
+ });
- /**
- * Get the windows platform object
- * @return {Platform}
- */
- getWindowsPlatform() {
- return platforms.find((p) => p.isWindows());
- }
+ if (!selected) {
+ throw new Error(`Unable to find platform (${name}) with architecture (${arch})`);
+ }
- /**
- * Get the mac platform object
- * @return {Platform}
- */
- getMacPlatform() {
- return platforms.find((p) => p.isMac());
+ return selected;
}
/**
@@ -136,16 +129,7 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
* @return {Platform}
*/
getPlatformForThisOs() {
- switch (getOsPlatform()) {
- case 'darwin':
- return this.getMacPlatform();
- case 'win32':
- return this.getWindowsPlatform();
- case 'linux':
- return this.getLinuxPlatform();
- default:
- throw new Error(`Unable to find platform for this os`);
- }
+ return this.getPlatform(os.platform(), os.arch());
}
/**
diff --git a/src/dev/build/lib/platform.js b/src/dev/build/lib/platform.js
index ac2faa7cbdf85..ab2672615e1c5 100644
--- a/src/dev/build/lib/platform.js
+++ b/src/dev/build/lib/platform.js
@@ -17,22 +17,26 @@
* under the License.
*/
-export function createPlatform(name) {
+export function createPlatform(name, architecture, buildName) {
return new (class Platform {
getName() {
return name;
}
- getNodeArch() {
- return `${name}-x64`;
+ getArchitecture() {
+ return architecture;
}
getBuildName() {
- return `${name}-x86_64`;
+ return buildName;
+ }
+
+ getNodeArch() {
+ return `${name}-${architecture}`;
}
isWindows() {
- return name === 'windows';
+ return name === 'win32';
}
isMac() {
diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js
index 31731e392e5cb..ff5c3b3a73dd3 100644
--- a/src/dev/build/tasks/clean_tasks.js
+++ b/src/dev/build/tasks/clean_tasks.js
@@ -201,45 +201,6 @@ export const CleanExtraBinScriptsTask = {
},
};
-export const CleanExtraBrowsersTask = {
- description: 'Cleaning extra browsers from platform-specific builds',
-
- async run(config, log, build) {
- const getBrowserPathsForPlatform = (platform) => {
- const reportingDir = 'x-pack/plugins/reporting';
- const chromiumDir = '.chromium';
- const chromiumPath = (p) =>
- build.resolvePathForPlatform(platform, reportingDir, chromiumDir, p);
- return (platforms) => {
- const paths = [];
- if (platforms.windows) {
- paths.push(chromiumPath('chromium-*-win32.zip'));
- paths.push(chromiumPath('chromium-*-windows.zip'));
- }
-
- if (platforms.darwin) {
- paths.push(chromiumPath('chromium-*-darwin.zip'));
- }
-
- if (platforms.linux) {
- paths.push(chromiumPath('chromium-*-linux.zip'));
- }
- return paths;
- };
- };
- for (const platform of config.getNodePlatforms()) {
- const getBrowserPaths = getBrowserPathsForPlatform(platform);
- if (platform.isWindows()) {
- await deleteAll(getBrowserPaths({ linux: true, darwin: true }), log);
- } else if (platform.isMac()) {
- await deleteAll(getBrowserPaths({ linux: true, windows: true }), log);
- } else if (platform.isLinux()) {
- await deleteAll(getBrowserPaths({ windows: true, darwin: true }), log);
- }
- }
- },
-};
-
export const CleanEmptyFoldersTask = {
description: 'Cleaning all empty folders recursively',
diff --git a/src/dev/build/tasks/create_archives_sources_task.js b/src/dev/build/tasks/create_archives_sources_task.js
index 53cf750f484a1..76f08bd3d2e4f 100644
--- a/src/dev/build/tasks/create_archives_sources_task.js
+++ b/src/dev/build/tasks/create_archives_sources_task.js
@@ -33,7 +33,7 @@ export const CreateArchivesSourcesTask = {
log.debug(
'Generic build source copied into',
- platform.getName(),
+ platform.getNodeArch(),
'specific build directory'
);
@@ -43,7 +43,7 @@ export const CreateArchivesSourcesTask = {
destination: build.resolvePathForPlatform(platform, 'node'),
});
- log.debug('Node.js copied into', platform.getName(), 'specific build directory');
+ log.debug('Node.js copied into', platform.getNodeArch(), 'specific build directory');
})
);
},
diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js
index be675b4aa6ca4..d96e745c10776 100644
--- a/src/dev/build/tasks/index.js
+++ b/src/dev/build/tasks/index.js
@@ -18,6 +18,7 @@
*/
export * from './bin';
+export * from './build_kibana_platform_plugins';
export * from './build_packages_task';
export * from './clean_tasks';
export * from './copy_source_task';
@@ -26,18 +27,18 @@ export * from './create_archives_task';
export * from './create_empty_dirs_and_files_task';
export * from './create_package_json_task';
export * from './create_readme_task';
+export * from './install_chromium';
export * from './install_dependencies_task';
export * from './license_file_task';
-export * from './nodejs';
export * from './nodejs_modules';
+export * from './nodejs';
export * from './notice_file_task';
export * from './optimize_task';
export * from './os_packages';
export * from './patch_native_modules_task';
+export * from './path_length_task';
export * from './transpile_babel_task';
export * from './transpile_scss_task';
+export * from './uuid_verification_task';
export * from './verify_env_task';
export * from './write_sha_sums_task';
-export * from './path_length_task';
-export * from './build_kibana_platform_plugins';
-export * from './uuid_verification_task';
diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js
new file mode 100644
index 0000000000000..c5878b23d43ae
--- /dev/null
+++ b/src/dev/build/tasks/install_chromium.js
@@ -0,0 +1,44 @@
+/*
+ * 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-next-line @kbn/eslint/no-restricted-paths
+import { installBrowser } from '../../../../x-pack/plugins/reporting/server/browsers/install';
+import { first } from 'rxjs/operators';
+
+export const InstallChromiumTask = {
+ description: 'Installing Chromium',
+
+ async run(config, log, build) {
+ if (build.isOss()) {
+ return;
+ } else {
+ for (const platform of config.getNodePlatforms()) {
+ log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`);
+
+ const { binaryPath$ } = installBrowser(
+ log,
+ build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'),
+ platform.getName(),
+ platform.getArchitecture()
+ );
+ await binaryPath$.pipe(first()).toPromise();
+ }
+ }
+ },
+};
diff --git a/src/dev/build/tasks/notice_file_task.js b/src/dev/build/tasks/notice_file_task.js
index 36a92f59314e2..59369c7cb5a3b 100644
--- a/src/dev/build/tasks/notice_file_task.js
+++ b/src/dev/build/tasks/notice_file_task.js
@@ -47,7 +47,7 @@ export const CreateNoticeFileTask = {
log.info('Generating build notice');
const { extractDir: nodeDir, version: nodeVersion } = getNodeDownloadInfo(
config,
- config.getLinuxPlatform()
+ config.getPlatform('linux', 'x64')
);
const notice = await generateBuildNoticeText({
diff --git a/src/dev/build/tasks/os_packages/run_fpm.js b/src/dev/build/tasks/os_packages/run_fpm.js
index 0496bcf08fb91..eb77da0e70176 100644
--- a/src/dev/build/tasks/os_packages/run_fpm.js
+++ b/src/dev/build/tasks/os_packages/run_fpm.js
@@ -22,7 +22,7 @@ import { resolve } from 'path';
import { exec } from '../../lib';
export async function runFpm(config, log, build, type, pkgSpecificFlags) {
- const linux = config.getLinuxPlatform();
+ const linux = config.getPlatform('linux', 'x64');
const version = config.getBuildVersion();
const resolveWithTrailingSlash = (...paths) => `${resolve(...paths)}/`;
diff --git a/src/dev/build/tasks/patch_native_modules_task.js b/src/dev/build/tasks/patch_native_modules_task.js
index fba33442fad10..a10010ed5255f 100644
--- a/src/dev/build/tasks/patch_native_modules_task.js
+++ b/src/dev/build/tasks/patch_native_modules_task.js
@@ -38,7 +38,7 @@ const packages = [
url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/linux-x64-64.gz',
sha256: 'f54f059035e71a7ccb3fa201080e260c41d228d13a8247974b4bb157691b6757',
},
- windows: {
+ win32: {
url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/win32-x64-64.gz',
sha256: 'de708446a8b802f4634c2cfef097c2625a2811fdcd8133dfd7b7c485f966caa9',
},
diff --git a/src/legacy/core_plugins/timelion/public/_app.scss b/src/legacy/core_plugins/timelion/public/_app.scss
index e44321f26e8dd..3142e1d23cf10 100644
--- a/src/legacy/core_plugins/timelion/public/_app.scss
+++ b/src/legacy/core_plugins/timelion/public/_app.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
.timApp {
position: relative;
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
index 2102b02194bc8..8b4c28a50b732 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
@@ -51,7 +51,7 @@ import {
suggest,
insertAtLocation,
} from './timelion_expression_input_helpers';
-import { comboBoxKeyCodes } from '@elastic/eui';
+import { comboBoxKeys } from '@elastic/eui';
import { npStart } from 'ui/new_platform';
const Parser = PEG.generate(grammar);
@@ -178,9 +178,9 @@ export function TimelionExpInput($http, $timeout) {
});
}
- function isNavigationalKey(keyCode) {
- const keyCodes = _.values(comboBoxKeyCodes);
- return keyCodes.includes(keyCode);
+ function isNavigationalKey(key) {
+ const keyCodes = _.values(comboBoxKeys);
+ return keyCodes.includes(key);
}
scope.onFocusInput = () => {
@@ -196,12 +196,12 @@ export function TimelionExpInput($http, $timeout) {
scope.onKeyDownInput = (e) => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation. The keyup handler will pull up suggestions.
- if (!isNavigationalKey(e.keyCode)) {
+ if (!isNavigationalKey(e.key)) {
return;
}
switch (e.keyCode) {
- case comboBoxKeyCodes.UP:
+ case comboBoxKeys.ARROW_UP:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
@@ -210,7 +210,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.DOWN:
+ case comboBoxKeys.ARROW_DOWN:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
@@ -219,7 +219,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.TAB:
+ case comboBoxKeys.TAB:
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) {
// Before letting the tab be handled to focus the next element
@@ -234,7 +234,7 @@ export function TimelionExpInput($http, $timeout) {
insertSuggestionIntoExpression(scope.suggestions.index);
break;
- case comboBoxKeyCodes.ENTER:
+ case comboBoxKeys.ENTER:
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
@@ -246,7 +246,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.ESCAPE:
+ case comboBoxKeys.ESCAPE:
e.preventDefault();
scope.suggestions.hide();
break;
@@ -255,7 +255,7 @@ export function TimelionExpInput($http, $timeout) {
scope.onKeyUpInput = (e) => {
// If the user isn't navigating, then we should update the suggestions based on their input.
- if (!isNavigationalKey(e.keyCode)) {
+ if (!isNavigationalKey(e.key)) {
getSuggestions();
}
};
diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
index 5466e7d43f566..f3b7ab29d8a14 100644
--- a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
+++ b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
@@ -22,7 +22,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../kbn_accessible_click';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
describe('kbnAccessibleClick directive', () => {
let $compile;
@@ -112,14 +112,14 @@ describe('kbnAccessibleClick directive', () => {
it(`on ENTER keyup`, () => {
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap
- e.keyCode = keyCodes.ENTER;
+ e.key = keys.ENTER;
element.trigger(e);
sinon.assert.calledOnce(scope.handleClick);
});
it(`on SPACE keyup`, () => {
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap
- e.keyCode = keyCodes.SPACE;
+ e.key = keys.SPACE;
element.trigger(e);
sinon.assert.calledOnce(scope.handleClick);
});
diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
index a8a6f0cd0db2f..ce1bf95bf0fb7 100644
--- a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
+++ b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
@@ -22,7 +22,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../kbn_ui_ace_keyboard_mode';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
describe('kbnUiAceKeyboardMode directive', () => {
let element;
@@ -48,7 +48,7 @@ describe('kbnUiAceKeyboardMode directive', () => {
const textarea = element.find('textarea');
sinon.spy(textarea[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ENTER;
+ ev.key = keys.ENTER;
element.find('.kbnUiAceKeyboardHint').trigger(ev);
expect(textarea[0].focus.called).to.be(true);
expect(
@@ -61,7 +61,7 @@ describe('kbnUiAceKeyboardMode directive', () => {
const hint = element.find('.kbnUiAceKeyboardHint');
sinon.spy(hint[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ESCAPE;
+ ev.key = keys.ESCAPE;
textarea.trigger(ev);
expect(hint[0].focus.called).to.be(true);
expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false);
@@ -101,7 +101,7 @@ describe('kbnUiAceKeyboardModeService', () => {
const textarea = element.find('textarea');
sinon.spy(textarea[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ENTER;
+ ev.key = keys.ENTER;
element.find('.kbnUiAceKeyboardHint').trigger(ev);
expect(textarea[0].focus.called).to.be(true);
expect(
@@ -114,7 +114,7 @@ describe('kbnUiAceKeyboardModeService', () => {
const hint = element.find('.kbnUiAceKeyboardHint');
sinon.spy(hint[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ESCAPE;
+ ev.key = keys.ESCAPE;
textarea.trigger(ev);
expect(hint[0].focus.called).to.be(true);
expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false);
diff --git a/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
index 9ffcbc426e49c..88b08beb5b3d0 100644
--- a/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
+++ b/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
@@ -33,7 +33,7 @@
import angular from 'angular';
import { uiModules } from '../modules';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
let aceKeyboardModeId = 0;
@@ -72,7 +72,7 @@ uiModules
}
hint.keydown((ev) => {
- if (ev.keyCode === keyCodes.ENTER) {
+ if (ev.key === keys.ENTER) {
ev.preventDefault();
startEditing();
}
@@ -103,7 +103,7 @@ uiModules
);
uiAceTextbox.keydown((ev) => {
- if (ev.keyCode === keyCodes.ESCAPE) {
+ if (ev.key === keys.ESCAPE) {
// If the autocompletion context menu is open then we want to let ESC close it but
// **not** exit out of editing mode.
if (!isAutoCompleterOpen) {
diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
index 0a12988fa5d90..d4273c0fdb207 100644
--- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
+++ b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
@@ -33,7 +33,7 @@ import chrome from 'ui/chrome';
import { ExitFullScreenButton } from './exit_full_screen_button';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
test('is rendered', () => {
const component = renderWithIntl( {}} />);
@@ -57,7 +57,7 @@ describe('onExitFullScreenMode', () => {
mountWithIntl( );
- const escapeKeyEvent = new KeyboardEvent('keydown', { keyCode: keyCodes.ESCAPE });
+ const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE });
document.dispatchEvent(escapeKeyEvent);
sinon.assert.calledOnce(onExitHandler);
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index b4b18e086e809..168dddf0253d9 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -150,7 +150,23 @@ export function uiRenderMixin(kbnServer, server, config) {
]),
];
- const kpPluginIds = Array.from(kbnServer.newPlatform.__internals.uiPlugins.public.keys());
+ const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins;
+ const kpPluginPublicPaths = new Map();
+ const kpPluginBundlePaths = new Set();
+
+ // recursively iterate over the kpUiPlugin ids and their required bundles
+ // to populate kpPluginPublicPaths and kpPluginBundlePaths
+ (function readKpPlugins(ids) {
+ for (const id of ids) {
+ if (kpPluginPublicPaths.has(id)) {
+ continue;
+ }
+
+ kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`);
+ kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`);
+ readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles);
+ }
+ })(kpUiPlugins.public.keys());
const jsDependencyPaths = [
...UiSharedDeps.jsDepFilenames.map(
@@ -160,9 +176,7 @@ export function uiRenderMixin(kbnServer, server, config) {
...(isCore ? [] : [`${dllBundlePath}/vendors_runtime.bundle.dll.js`, ...dllJsChunks]),
`${regularBundlePath}/core/core.entry.js`,
- ...kpPluginIds.map(
- (pluginId) => `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`
- ),
+ ...kpPluginBundlePaths,
];
// These paths should align with the bundle routes configured in
@@ -170,13 +184,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const publicPathMap = JSON.stringify({
core: `${regularBundlePath}/core/`,
'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`,
- ...kpPluginIds.reduce(
- (acc, pluginId) => ({
- ...acc,
- [pluginId]: `${regularBundlePath}/plugin/${pluginId}/`,
- }),
- {}
- ),
+ ...Object.fromEntries(kpPluginPublicPaths),
});
const bootstrap = new AppBootstrap({
diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json
index e6ca6e797ba45..8cf9b9c656d8f 100644
--- a/src/plugins/advanced_settings/kibana.json
+++ b/src/plugins/advanced_settings/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["management"]
+ "requiredPlugins": ["management"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
index 5fddaa178f580..8d768d200fdd2 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
+++ b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
// TODO #64541
diff --git a/src/plugins/bfetch/kibana.json b/src/plugins/bfetch/kibana.json
index 462d2f4b8bb7d..9f9f2176af671 100644
--- a/src/plugins/bfetch/kibana.json
+++ b/src/plugins/bfetch/kibana.json
@@ -2,5 +2,6 @@
"id": "bfetch",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json
index 9f4433e7099d8..c4643d541c31c 100644
--- a/src/plugins/charts/kibana.json
+++ b/src/plugins/charts/kibana.json
@@ -2,5 +2,6 @@
"id": "charts",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "data"]
}
diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json
index 57de385ba565c..031aa00eb6613 100644
--- a/src/plugins/console/kibana.json
+++ b/src/plugins/console/kibana.json
@@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["devTools", "home"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"]
}
diff --git a/src/plugins/console/public/application/containers/console_history/console_history.tsx b/src/plugins/console/public/application/containers/console_history/console_history.tsx
index 8ec8b9c61bf03..433ad15990d77 100644
--- a/src/plugins/console/public/application/containers/console_history/console_history.tsx
+++ b/src/plugins/console/public/application/containers/console_history/console_history.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { memoize } from 'lodash';
import moment from 'moment';
import {
- keyCodes,
+ keys,
EuiSpacer,
EuiIcon,
EuiTitle,
@@ -125,17 +125,17 @@ export function ConsoleHistory({ close }: Props) {
{
- if (ev.keyCode === keyCodes.ENTER) {
+ if (ev.key === keys.ENTER) {
restoreRequestFromHistory(selectedReq.current);
return;
}
let currentIdx = selectedIndex;
- if (ev.keyCode === keyCodes.UP) {
+ if (ev.key === keys.ARROW_UP) {
ev.preventDefault();
--currentIdx;
- } else if (ev.keyCode === keyCodes.DOWN) {
+ } else if (ev.key === keys.ARROW_DOWN) {
ev.preventDefault();
++currentIdx;
}
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
index 6d4f532887cd9..880069d8ebc7a 100644
--- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import { parse } from 'query-string';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
-import { useUIAceKeyboardMode } from '../../../../../../../es_ui_shared/public';
+import { ace } from '../../../../../../../es_ui_shared/public';
// @ts-ignore
import { retrieveAutoCompleteInfo, clearSubscriptions } from '../../../../../lib/mappings/mappings';
import { ConsoleMenu } from '../../../../components';
@@ -38,6 +38,8 @@ import { subscribeResizeChecker } from '../subscribe_console_resize_checker';
import { applyCurrentSettings } from './apply_editor_settings';
import { registerCommands } from './keyboard_shortcuts';
+const { useUIAceKeyboardMode } = ace;
+
export interface EditorProps {
initialTextValue: string;
}
diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss
index c41df24912c2a..baf4cf1cbd143 100644
--- a/src/plugins/console/public/styles/_app.scss
+++ b/src/plugins/console/public/styles/_app.scss
@@ -1,5 +1,5 @@
// TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules).
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
// This value is calculated to static value using SCSS because calc in calc has issues in IE11
$headerHeightOffset: $euiHeaderHeightCompensation * 2;
diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json
index 4cd8f3c7d981f..1b38c6d124fe1 100644
--- a/src/plugins/dashboard/kibana.json
+++ b/src/plugins/dashboard/kibana.json
@@ -12,5 +12,6 @@
],
"optionalPlugins": ["home", "share", "usageCollection"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "home"]
}
diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json
index 3e5d96a4bc47b..2ffd0688b134e 100644
--- a/src/plugins/data/kibana.json
+++ b/src/plugins/data/kibana.json
@@ -8,5 +8,11 @@
"uiActions"
],
"optionalPlugins": ["usageCollection"],
- "extraPublicDirs": ["common", "common/utils/abort_utils"]
+ "extraPublicDirs": ["common", "common/utils/abort_utils"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "kibanaLegacy",
+ "inspector"
+ ]
}
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 4a78a80e3ae55..0a2bd094f8a63 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -11,7 +11,6 @@ import { ApplicationStart } from 'kibana/public';
import { Assign } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import Boom from 'boom';
-import { Breadcrumb } from '@elastic/eui';
import { BulkIndexDocumentsParams } from 'elasticsearch';
import { CatAliasesParams } from 'elasticsearch';
import { CatAllocationParams } from 'elasticsearch';
@@ -48,6 +47,7 @@ import { DeleteScriptParams } from 'elasticsearch';
import { DeleteTemplateParams } from 'elasticsearch';
import { Ensure } from '@kbn/utility-types';
import { ErrorToastOptions } from 'src/core/public/notifications';
+import { EuiBreadcrumb } from '@elastic/eui';
import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json
index 14dd399697b56..041f362bf0623 100644
--- a/src/plugins/discover/kibana.json
+++ b/src/plugins/discover/kibana.json
@@ -1,7 +1,6 @@
{
"id": "discover",
"version": "kibana",
- "optionalPlugins": ["share"],
"server": true,
"ui": true,
"requiredPlugins": [
@@ -14,5 +13,11 @@
"uiActions",
"visualizations"
],
- "optionalPlugins": ["home", "share"]
+ "optionalPlugins": ["home", "share"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "home",
+ "savedObjects",
+ "kibanaReact"
+ ]
}
diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json
index 332237d19e218..3163c4bde4704 100644
--- a/src/plugins/embeddable/kibana.json
+++ b/src/plugins/embeddable/kibana.json
@@ -10,5 +10,9 @@
],
"extraPublicDirs": [
"public/lib/test_samples"
+ ],
+ "requiredBundles": [
+ "savedObjects",
+ "kibanaReact"
]
}
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
new file mode 100644
index 0000000000000..5b637224c1784
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
@@ -0,0 +1,24 @@
+.kbnUiAceKeyboardHint {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background: transparentize($euiColorEmptyShade, 0.3);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ opacity: 0;
+
+ &:focus {
+ opacity: 1;
+ border: 2px solid $euiColorPrimary;
+ z-index: $euiZLevel1;
+ }
+
+ &.kbnUiAceKeyboardHint-isInactive {
+ display: none;
+ }
+}
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
new file mode 100644
index 0000000000000..72d0d6d85ee6e
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
diff --git a/src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
similarity index 95%
rename from src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx
rename to src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
index a93906d50b64a..d0d1aa1d8db15 100644
--- a/src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
@@ -16,9 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import React, { useEffect, useRef } from 'react';
import * as ReactDOM from 'react-dom';
-import { keyCodes, EuiText } from '@elastic/eui';
+import { keys, EuiText } from '@elastic/eui';
+
+import './_ui_ace_keyboard_mode.scss';
const OverlayText = () => (
// The point of this element is for accessibility purposes, so ignore eslint error
@@ -37,7 +40,7 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n
useEffect(() => {
function onDismissOverlay(event: KeyboardEvent) {
- if (event.keyCode === keyCodes.ENTER) {
+ if (event.key === keys.ENTER) {
event.preventDefault();
aceTextAreaElement!.focus();
}
@@ -63,7 +66,7 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n
};
const aceKeydownListener = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) {
+ if (event.key === keys.ESCAPE && !autoCompleteVisibleRef.current) {
event.preventDefault();
event.stopPropagation();
enableOverlay();
diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json
index 980f43ea46a68..eab7355d66f09 100644
--- a/src/plugins/es_ui_shared/kibana.json
+++ b/src/plugins/es_ui_shared/kibana.json
@@ -10,5 +10,8 @@
"static/forms/helpers",
"static/forms/components",
"static/forms/helpers/field_validators/types"
+ ],
+ "requiredBundles": [
+ "data"
]
}
diff --git a/src/plugins/es_ui_shared/public/ace/index.ts b/src/plugins/es_ui_shared/public/ace/index.ts
new file mode 100644
index 0000000000000..98507fa2fd6ad
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/ace/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+export { useUIAceKeyboardMode } from '../../__packages_do_not_import__/ace';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index d472b7e462057..98a305fe68f08 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -23,6 +23,7 @@
*/
import * as Forms from './forms';
import * as Monaco from './monaco';
+import * as ace from './ace';
export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor';
@@ -41,8 +42,6 @@ export {
export { indices } from './indices';
-export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
-
export {
installXJsonMode,
XJsonMode,
@@ -66,7 +65,7 @@ export {
useAuthorizationContext,
} from './authorization';
-export { Monaco, Forms };
+export { Monaco, Forms, ace };
export { extractQueryParams } from './url';
diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json
index 4774c69cc29ff..5163331088103 100644
--- a/src/plugins/expressions/kibana.json
+++ b/src/plugins/expressions/kibana.json
@@ -6,5 +6,10 @@
"requiredPlugins": [
"bfetch"
],
- "extraPublicDirs": ["common", "common/fonts"]
+ "extraPublicDirs": ["common", "common/fonts"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "inspector",
+ "data"
+ ]
}
diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json
index 1c4b44a946e62..74bd3625ca964 100644
--- a/src/plugins/home/kibana.json
+++ b/src/plugins/home/kibana.json
@@ -4,5 +4,8 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "kibanaLegacy"],
- "optionalPlugins": ["usageCollection", "telemetry"]
+ "optionalPlugins": ["usageCollection", "telemetry"],
+ "requiredBundles": [
+ "kibanaReact"
+ ]
}
diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json
index 23adef2626a72..d0ad6a96065c3 100644
--- a/src/plugins/index_pattern_management/kibana.json
+++ b/src/plugins/index_pattern_management/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["management", "data", "kibanaLegacy"]
+ "requiredPlugins": ["management", "data", "kibanaLegacy"],
+ "requiredBundles": ["kibanaReact", "kibanaUtils"]
}
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
index 70200e03c0dbe..6d79515c172fe 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
@@ -52,6 +53,7 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
@@ -66,6 +68,7 @@ exports[`CreateIndexPatternWizard renders the empty state when there are no indi
/>
@@ -107,6 +110,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2
@@ -148,6 +152,7 @@ exports[`CreateIndexPatternWizard renders when there are no indices but there ar
@@ -162,6 +167,7 @@ exports[`CreateIndexPatternWizard shows system indices even if there are no othe
/>
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
index ab5a253a98e29..e43ee2e55eeca 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
@@ -21,7 +21,7 @@ import React, { ReactElement } from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { Table, TableProps, TableState } from './table';
-import { EuiTableFieldDataColumnType, keyCodes } from '@elastic/eui';
+import { EuiTableFieldDataColumnType, keys } from '@elastic/eui';
import { IIndexPattern } from 'src/plugins/data/public';
import { SourceFiltersTableFilter } from '../../types';
@@ -250,7 +250,7 @@ describe('Table', () => {
);
// Press the enter key
- filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ENTER });
+ filterNameTableCell.find('EuiFieldText').simulate('keydown', { key: keys.ENTER });
expect(saveFilter).toBeCalled();
// It should reset
@@ -289,7 +289,7 @@ describe('Table', () => {
);
// Press the ESCAPE key
- filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ESCAPE });
+ filterNameTableCell.find('EuiFieldText').simulate('keydown', { key: keys.ESCAPE });
expect(saveFilter).not.toBeCalled();
// It should reset
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
index 04998d9f7dafe..f73d756f28116 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
@@ -20,7 +20,7 @@
import React, { Component } from 'react';
import {
- keyCodes,
+ keys,
EuiBasicTableColumn,
EuiInMemoryTable,
EuiFieldText,
@@ -111,15 +111,15 @@ export class Table extends Component {
onEditingFilterChange = (e: React.ChangeEvent) =>
this.setState({ editingFilterValue: e.target.value });
- onEditFieldKeyDown = ({ keyCode }: React.KeyboardEvent) => {
- if (keyCodes.ENTER === keyCode && this.state.editingFilterId && this.state.editingFilterValue) {
+ onEditFieldKeyDown = ({ key }: React.KeyboardEvent) => {
+ if (keys.ENTER === key && this.state.editingFilterId && this.state.editingFilterValue) {
this.props.saveFilter({
clientId: this.state.editingFilterId,
value: this.state.editingFilterValue,
});
this.stopEditingFilter();
}
- if (keyCodes.ESCAPE === keyCode) {
+ if (keys.ESCAPE === key) {
this.stopEditingFilter();
}
};
diff --git a/src/plugins/input_control_vis/kibana.json b/src/plugins/input_control_vis/kibana.json
index 4a4ec328c1352..6928eb19d02e1 100644
--- a/src/plugins/input_control_vis/kibana.json
+++ b/src/plugins/input_control_vis/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "expressions", "visualizations"]
+ "requiredPlugins": ["data", "expressions", "visualizations"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json
index 99a38d2928df6..90e5d60250728 100644
--- a/src/plugins/inspector/kibana.json
+++ b/src/plugins/inspector/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": false,
"ui": true,
- "extraPublicDirs": ["common", "common/adapters/request"]
+ "extraPublicDirs": ["common", "common/adapters/request"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
index adea7831d6b80..2632afff2f63b 100644
--- a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
+++ b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
@@ -269,7 +269,9 @@ exports[`Inspector Data View component should render empty state 1`] = `
-
+
diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
index ba1363ef06285..2dbf4002da748 100644
--- a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
+++ b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { accessibleClickKeys, keyCodes } from '@elastic/eui';
+import { accessibleClickKeys, keys } from '@elastic/eui';
export function KbnAccessibleClickProvider() {
return {
@@ -24,7 +24,7 @@ export function KbnAccessibleClickProvider() {
controller: ($element) => {
$element.on('keydown', (e) => {
// Prevent a scroll from occurring if the user has hit space.
- if (e.keyCode === keyCodes.SPACE) {
+ if (e.key === keys.SPACE) {
e.preventDefault();
}
});
@@ -60,7 +60,7 @@ export function KbnAccessibleClickProvider() {
element.on('keyup', (e) => {
// Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress.
- if (accessibleClickKeys[e.keyCode]) {
+ if (accessibleClickKeys[e.key]) {
// Delegate to the click handler on the element (assumed to be ng-click).
element.click();
}
diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json
index 0add1bee84ae0..a507fe457b633 100644
--- a/src/plugins/kibana_react/kibana.json
+++ b/src/plugins/kibana_react/kibana.json
@@ -1,5 +1,6 @@
{
"id": "kibanaReact",
"version": "kibana",
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
index 8f264a6bafca7..03af32712afa5 100644
--- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
+++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
@@ -20,7 +20,7 @@
import React from 'react';
import sinon from 'sinon';
import { ExitFullScreenButton } from './exit_full_screen_button';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { mount } from 'enzyme';
test('is rendered', () => {
@@ -45,7 +45,7 @@ describe('onExitFullScreenMode', () => {
mount( );
- const escapeKeyEvent = new KeyboardEvent('keydown', { keyCode: keyCodes.ESCAPE } as any);
+ const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any);
document.dispatchEvent(escapeKeyEvent);
sinon.assert.calledOnce(onExitHandler);
diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
index 2a359b7cca5d1..3a1a34f1fc3be 100644
--- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
+++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
@@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import React, { PureComponent } from 'react';
-import { EuiScreenReaderOnly, keyCodes } from '@elastic/eui';
+import { EuiScreenReaderOnly, keys } from '@elastic/eui';
import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
export interface ExitFullScreenButtonProps {
@@ -30,7 +30,7 @@ import './index.scss';
class ExitFullScreenButtonUi extends PureComponent {
public onKeyDown = (e: KeyboardEvent) => {
- if (e.keyCode === keyCodes.ESCAPE) {
+ if (e.key === keys.ESCAPE) {
this.props.onExitFullScreenMode();
}
};
diff --git a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
index 45fe20095fd83..a44ed04c7bc79 100644
--- a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
+++ b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
@@ -19,7 +19,7 @@
import React, { Children, ReactNode, useRef, useState, useCallback, useEffect } from 'react';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { PanelContextProvider } from '../context';
import { Resizer, ResizerMouseEvent, ResizerKeyDownEvent } from '../components/resizer';
import { PanelRegistry } from '../registry';
@@ -70,16 +70,16 @@ export function PanelsContainer({
const handleKeyDown = useCallback(
(ev: ResizerKeyDownEvent) => {
- const { keyCode } = ev;
+ const { key } = ev;
- if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) {
+ if (key === keys.ARROW_LEFT || key === keys.ARROW_RIGHT) {
ev.preventDefault();
const { current: registry } = registryRef;
const [left, right] = registry.getPanels();
- const leftPercent = left.width - (keyCode === keyCodes.LEFT ? 1 : -1);
- const rightPercent = right.width - (keyCode === keyCodes.RIGHT ? 1 : -1);
+ const leftPercent = left.width - (key === keys.ARROW_LEFT ? 1 : -1);
+ const rightPercent = right.width - (key === keys.ARROW_RIGHT ? 1 : -1);
left.setWidth(leftPercent);
right.setWidth(rightPercent);
diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json
index cc411a8c6a25c..f48158e98ff3f 100644
--- a/src/plugins/management/kibana.json
+++ b/src/plugins/management/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["kibanaLegacy", "home"]
+ "requiredPlugins": ["kibanaLegacy", "home"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json
index cd503883164ac..d9bf33e661368 100644
--- a/src/plugins/maps_legacy/kibana.json
+++ b/src/plugins/maps_legacy/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"configPath": ["map"],
"ui": true,
- "server": true
+ "server": true,
+ "requiredBundles": ["kibanaReact", "charts"]
}
diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json
index ac7e1f8659d66..6e1980c327dc0 100644
--- a/src/plugins/region_map/kibana.json
+++ b/src/plugins/region_map/kibana.json
@@ -11,5 +11,10 @@
"mapsLegacy",
"kibanaLegacy",
"data"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "charts"
]
}
diff --git a/src/plugins/saved_objects/kibana.json b/src/plugins/saved_objects/kibana.json
index 7ae1b84eecad8..589aafbd2aaf5 100644
--- a/src/plugins/saved_objects/kibana.json
+++ b/src/plugins/saved_objects/kibana.json
@@ -3,5 +3,9 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data"]
+ "requiredPlugins": ["data"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
+ ]
}
diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json
index 6184d890c415c..0270c1d8f5d39 100644
--- a/src/plugins/saved_objects_management/kibana.json
+++ b/src/plugins/saved_objects_management/kibana.json
@@ -5,5 +5,6 @@
"ui": true,
"requiredPlugins": ["home", "management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover"],
- "extraPublicDirs": ["public/lib"]
+ "extraPublicDirs": ["public/lib"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
index 6b209a62e1b98..6256e5fcd49c5 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
@@ -21,7 +21,7 @@ import React from 'react';
import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
// @ts-expect-error
import { findTestSubject } from '@elastic/eui/lib/test';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { httpServiceMock } from '../../../../../../core/public/mocks';
import { actionServiceMock } from '../../../services/action_service.mock';
import { Table, TableProps } from './table';
@@ -100,14 +100,14 @@ describe('Table', () => {
const searchBar = findTestSubject(component, 'savedObjectSearchBar');
// Send invalid query
- searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: '?' } });
+ searchBar.simulate('keyup', { key: keys.ENTER, target: { value: '?' } });
expect(onQueryChangeMock).toHaveBeenCalledTimes(0);
expect(component.state().isSearchTextValid).toBe(false);
onQueryChangeMock.mockReset();
// Send valid query to ensure component can recover from invalid query
- searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: 'I am valid' } });
+ searchBar.simulate('keyup', { key: keys.ENTER, target: { value: 'I am valid' } });
expect(onQueryChangeMock).toHaveBeenCalledTimes(1);
expect(component.state().isSearchTextValid).toBe(true);
});
diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json
index dce2ac9281aba..7760ea321992d 100644
--- a/src/plugins/share/kibana.json
+++ b/src/plugins/share/kibana.json
@@ -2,5 +2,6 @@
"id": "share",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json
index a497597762520..520ca6076dbbd 100644
--- a/src/plugins/telemetry/kibana.json
+++ b/src/plugins/telemetry/kibana.json
@@ -9,5 +9,9 @@
],
"extraPublicDirs": [
"common/constants"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
]
}
diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json
index bb8ef5a246549..9881a2dd72308 100644
--- a/src/plugins/tile_map/kibana.json
+++ b/src/plugins/tile_map/kibana.json
@@ -11,5 +11,10 @@
"mapsLegacy",
"kibanaLegacy",
"data"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "charts"
]
}
diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json
index 907cbabbdf9c9..7b24b3cc5c48b 100644
--- a/src/plugins/ui_actions/kibana.json
+++ b/src/plugins/ui_actions/kibana.json
@@ -5,5 +5,8 @@
"ui": true,
"extraPublicDirs": [
"public/tests/test_samples"
+ ],
+ "requiredBundles": [
+ "kibanaReact"
]
}
diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json
index ae86b6c5d7ad1..6ef78018c7d7f 100644
--- a/src/plugins/usage_collection/kibana.json
+++ b/src/plugins/usage_collection/kibana.json
@@ -3,5 +3,8 @@
"configPath": ["usageCollection"],
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaUtils"
+ ]
}
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
index c41315e7bc0dc..bcbc5afec1fdc 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
@@ -20,7 +20,7 @@
import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react';
import { get, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { keys, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EventEmitter } from 'events';
import {
@@ -119,7 +119,7 @@ function DefaultEditorSideBar({
const onSubmit: KeyboardEventHandler = useCallback(
(event) => {
- if (event.ctrlKey && event.keyCode === keyCodes.ENTER) {
+ if (event.ctrlKey && event.key === keys.ENTER) {
event.preventDefault();
event.stopPropagation();
diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json
index d52e22118ccf0..9241f5eeee837 100644
--- a/src/plugins/vis_type_markdown/kibana.json
+++ b/src/plugins/vis_type_markdown/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"ui": true,
"server": true,
- "requiredPlugins": ["expressions", "visualizations"]
+ "requiredPlugins": ["expressions", "visualizations"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts"]
}
diff --git a/src/plugins/vis_type_metric/kibana.json b/src/plugins/vis_type_metric/kibana.json
index 24135d257b317..b2ebc91471e9d 100644
--- a/src/plugins/vis_type_metric/kibana.json
+++ b/src/plugins/vis_type_metric/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "visualizations", "charts","expressions"]
+ "requiredPlugins": ["data", "visualizations", "charts","expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
index 79876377c8e44..267d92abe2c75 100644
--- a/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
+++ b/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
@@ -20,7 +20,7 @@
import React, { Component, KeyboardEvent } from 'react';
import classNames from 'classnames';
-import { EuiKeyboardAccessible, keyCodes } from '@elastic/eui';
+import { EuiKeyboardAccessible, keys } from '@elastic/eui';
import { MetricVisMetric } from '../types';
@@ -39,7 +39,7 @@ export class MetricVisValue extends Component {
};
onKeyPress = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ENTER) {
+ if (event.key === keys.ENTER) {
this.onClick();
}
};
diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json
index ed098d7161403..b3c1556429077 100644
--- a/src/plugins/vis_type_table/kibana.json
+++ b/src/plugins/vis_type_table/kibana.json
@@ -8,5 +8,11 @@
"visualizations",
"data",
"kibanaLegacy"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "share",
+ "charts"
]
}
diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json
index dbc9a1b9ef692..86f72ebfa936d 100644
--- a/src/plugins/vis_type_tagcloud/kibana.json
+++ b/src/plugins/vis_type_tagcloud/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"ui": true,
"server": true,
- "requiredPlugins": ["data", "expressions", "visualizations", "charts"]
+ "requiredPlugins": ["data", "expressions", "visualizations", "charts"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timelion/kibana.json b/src/plugins/vis_type_timelion/kibana.json
index 85c282c51a2e7..6946568f5d809 100644
--- a/src/plugins/vis_type_timelion/kibana.json
+++ b/src/plugins/vis_type_timelion/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["visualizations", "data", "expressions"]
+ "requiredPlugins": ["visualizations", "data", "expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json
index 9053d2543e0d0..f2284726c463f 100644
--- a/src/plugins/vis_type_timeseries/kibana.json
+++ b/src/plugins/vis_type_timeseries/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
index eae354f7cc8ec..db6024d48be12 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
@@ -20,7 +20,7 @@
import React from 'react';
import { collectionActions } from './lib/collection_actions';
import { ColorRules } from './color_rules';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
@@ -63,9 +63,9 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js'
collectionActions.handleChange = jest.fn();
const wrapper = mountWithIntl( );
const operatorInput = findTestSubject(wrapper, 'colorRuleOperator');
- operatorInput.simulate('keyDown', { keyCode: keyCodes.DOWN });
- operatorInput.simulate('keyDown', { keyCode: keyCodes.DOWN });
- operatorInput.simulate('keyDown', { keyCode: keyCodes.ENTER });
+ operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
+ operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
+ operatorInput.simulate('keyDown', { key: keys.ENTER });
expect(collectionActions.handleChange.mock.calls[0][1].operator).toEqual('gt');
const numberInput = findTestSubject(wrapper, 'colorRuleValue');
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
index 23a9555da2452..9c2b947bda08e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
@@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { get } from 'lodash';
-import { keyCodes, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
+import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import {
getInterval,
@@ -96,11 +96,11 @@ class VisEditorVisualizationUI extends Component {
* defined minimum width (MIN_CHART_HEIGHT).
*/
onSizeHandleKeyDown = (ev) => {
- const { keyCode } = ev;
- if (keyCode === keyCodes.UP || keyCode === keyCodes.DOWN) {
+ const { key } = ev;
+ if (key === keys.ARROW_UP || key === keys.ARROW_DOWN) {
ev.preventDefault();
this.setState((prevState) => {
- const newHeight = prevState.height + (keyCode === keyCodes.UP ? -15 : 15);
+ const newHeight = prevState.height + (key === keys.ARROW_UP ? -15 : 15);
return {
height: Math.max(MIN_CHART_HEIGHT, newHeight),
};
diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json
index f1f82e7f5b7ad..d7a92de627a99 100644
--- a/src/plugins/vis_type_vega/kibana.json
+++ b/src/plugins/vis_type_vega/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"]
+ "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_vislib/kibana.json b/src/plugins/vis_type_vislib/kibana.json
index cad0ebe01494a..7cba2e0d6a6b4 100644
--- a/src/plugins/vis_type_vislib/kibana.json
+++ b/src/plugins/vis_type_vislib/kibana.json
@@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"],
- "optionalPlugins": ["visTypeXy"]
+ "optionalPlugins": ["visTypeXy"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
index f7e44ed278787..129fdd2ade9bd 100644
--- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
+++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
@@ -21,7 +21,7 @@ import classNames from 'classnames';
import { compact, uniqBy, map, every, isUndefined } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui';
+import { EuiPopoverProps, EuiIcon, keys, htmlIdGenerator } from '@elastic/eui';
import { getDataActions } from '../../../services';
import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models';
@@ -75,7 +75,7 @@ export class VisLegend extends PureComponent {
};
setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => {
- if ((event as KeyboardEvent).keyCode && (event as KeyboardEvent).keyCode !== keyCodes.ENTER) {
+ if ((event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) {
return;
}
@@ -106,11 +106,7 @@ export class VisLegend extends PureComponent {
};
toggleDetails = (label: string | null) => (event?: BaseSyntheticEvent) => {
- if (
- event &&
- (event as KeyboardEvent).keyCode &&
- (event as KeyboardEvent).keyCode !== keyCodes.ENTER
- ) {
+ if (event && (event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) {
return;
}
this.setState({ selectedLabel: this.state.selectedLabel === label ? null : label });
diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
index 70b7a8ee335db..b440384899d5f 100644
--- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
+++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
@@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiPopover,
- keyCodes,
+ keys,
EuiIcon,
EuiSpacer,
EuiButtonEmpty,
@@ -67,7 +67,7 @@ const VisLegendItemComponent = ({
* This will close the details panel of this legend entry when pressing Escape.
*/
const onLegendEntryKeydown = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE) {
+ if (event.key === keys.ESCAPE) {
event.preventDefault();
event.stopPropagation();
onSelect(null)();
diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json
index f3f9cbd8341ec..da3edfbdd3bf5 100644
--- a/src/plugins/visualizations/kibana.json
+++ b/src/plugins/visualizations/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"]
+ "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"],
+ "requiredBundles": ["kibanaUtils", "discover", "savedObjects"]
}
diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
index 53ef164685a1c..5458c88974572 100644
--- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
+++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
@@ -139,7 +139,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
2 types found
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -562,121 +567,126 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
>
2 types found
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -813,121 +823,126 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
>
2 types found
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -1201,261 +1216,272 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
-
-
- Vis with alias Url
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
-
+ Vis with alias Url
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
-
-
-
-
+ type="popout"
+ >
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with alias Url
-
-
-
-
-
-
- Vis with search
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with alias Url
+
+
+
+
+
+
+
-
+ Vis with search
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with search
-
-
-
-
-
-
- Vis Type 1
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with search
+
+
+
+
+
+
+
-
+ Vis Type 1
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis Type 1
-
-
-
-
-
-
+
+ Vis Type 1
+
+
+
+
+
+
+
@@ -1683,7 +1709,7 @@ exports[`NewVisModal should render as expected 1`] = `
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2073,120 +2104,125 @@ exports[`NewVisModal should render as expected 1`] = `
aria-live="polite"
class="euiScreenReaderOnly"
/>
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2307,120 +2343,125 @@ exports[`NewVisModal should render as expected 1`] = `
aria-live="polite"
class="euiScreenReaderOnly"
/>
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2643,261 +2684,272 @@ exports[`NewVisModal should render as expected 1`] = `
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
-
-
- Vis Type 1
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
-
+ Vis Type 1
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis Type 1
-
-
-
-
-
-
- Vis with alias Url
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis Type 1
+
+
+
+
+
+
+
-
+ Vis with alias Url
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
-
-
-
-
+ type="popout"
+ >
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with alias Url
-
-
-
-
-
-
- Vis with search
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with alias Url
+
+
+
+
+
+
+
-
+ Vis with search
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with search
-
-
-
-
-
-
+
+ Vis with search
+
+
+
+
+
+
+
diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json
index c27cfec24b332..520d1e1daa6fe 100644
--- a/src/plugins/visualize/kibana.json
+++ b/src/plugins/visualize/kibana.json
@@ -11,5 +11,11 @@
"visualizations",
"embeddable"
],
- "optionalPlugins": ["home", "share"]
+ "optionalPlugins": ["home", "share"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "home",
+ "discover"
+ ]
}
diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts
index 867e78dfad8dc..0611c80f59b92 100644
--- a/test/functional/services/remote/webdriver.ts
+++ b/test/functional/services/remote/webdriver.ts
@@ -28,7 +28,7 @@ import { delay } from 'bluebird';
import chromeDriver from 'chromedriver';
// @ts-ignore types not available
import geckoDriver from 'geckodriver';
-import { Builder, Capabilities, logging } from 'selenium-webdriver';
+import { Builder, logging } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import firefox from 'selenium-webdriver/firefox';
import edge from 'selenium-webdriver/edge';
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
index f0c1c3a34fbc0..7eafb185617c4 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
@@ -9,5 +9,8 @@
"expressions"
],
"server": false,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "inspector"
+ ]
}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
index 535aecba26770..f3e5520a14fe2 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
diff --git a/test/plugin_functional/plugins/app_link_test/kibana.json b/test/plugin_functional/plugins/app_link_test/kibana.json
index 8cdc464abfec1..5384d4fee1508 100644
--- a/test/plugin_functional/plugins/app_link_test/kibana.json
+++ b/test/plugin_functional/plugins/app_link_test/kibana.json
@@ -3,5 +3,6 @@
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json b/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
index 109afbcd5dabd..08ce182aa0293 100644
--- a/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
+++ b/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
@@ -5,5 +5,6 @@
"configPath": ["kbn_sample_panel_action"],
"server": false,
"ui": true,
- "requiredPlugins": ["uiActions", "embeddable"]
-}
\ No newline at end of file
+ "requiredPlugins": ["uiActions", "embeddable"],
+ "requiredBundles": ["kibanaReact"]
+}
diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
index 612ae3806177c..b9c5b3bc5b836 100644
--- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
+++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
index 0a6b5fb185d30..95fafdf221c64 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh
index 3e49edc8e6ae5..2310a35f94f33 100755
--- a/test/scripts/jenkins_build_kibana.sh
+++ b/test/scripts/jenkins_build_kibana.sh
@@ -2,16 +2,10 @@
source src/dev/ci_setup/setup_env.sh
-echo " -> building examples separate from test plugins"
+echo " -> building kibana platform plugins"
node scripts/build_kibana_platform_plugins \
--oss \
- --examples \
- --verbose;
-
-echo " -> building test plugins"
-node scripts/build_kibana_platform_plugins \
- --oss \
- --no-examples \
+ --filter '!alertingExample' \
--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 58ef6a42d3fe4..c962b962b1e5e 100755
--- a/test/scripts/jenkins_xpack_build_kibana.sh
+++ b/test/scripts/jenkins_xpack_build_kibana.sh
@@ -3,14 +3,8 @@
cd "$KIBANA_DIR"
source src/dev/ci_setup/setup_env.sh
-echo " -> building examples separate from test plugins"
+echo " -> building kibana platform 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/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh
index 726af43f28e31..ac567a188a6d4 100755
--- a/test/scripts/jenkins_xpack_visual_regression.sh
+++ b/test/scripts/jenkins_xpack_visual_regression.sh
@@ -22,5 +22,5 @@ yarn percy exec -t 10000 -- -- \
# cd "$KIBANA_DIR"
# source "test/scripts/jenkins_xpack_page_load_metrics.sh"
-cd "$XPACK_DIR"
-source "$KIBANA_DIR/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
+cd "$KIBANA_DIR"
+source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
diff --git a/x-pack/.gitignore b/x-pack/.gitignore
index 68262c4bf734b..e181caf2b1a49 100644
--- a/x-pack/.gitignore
+++ b/x-pack/.gitignore
@@ -8,7 +8,7 @@
/test/reporting/configs/failure_debug/
/legacy/plugins/reporting/.chromium/
/legacy/plugins/reporting/.phantom/
-/plugins/reporting/.chromium/
+/plugins/reporting/chromium/
/plugins/reporting/.phantom/
/.aws-config.json
/.env
diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md
index 72e41afc80c95..ce7e110a5f914 100644
--- a/x-pack/build_chromium/README.md
+++ b/x-pack/build_chromium/README.md
@@ -20,7 +20,8 @@ You'll need access to our GCP account, which is where we have two machines provi
Chromium is built via a build tool called "ninja". The build can be configured by specifying build flags either in an "args.gn" file or via commandline args. We have an "args.gn" file per platform:
- mac: darwin/args.gn
-- linux: linux/args.gn
+- linux 64bit: linux-x64/args.gn
+- ARM 64bit: linux-aarch64/args.gn
- windows: windows/args.gn
The various build flags are not well documented. Some are documented [here](https://www.chromium.org/developers/gn-build-configuration). Some, such as `enable_basic_printing = false`, I only found by poking through 3rd party build scripts.
@@ -65,15 +66,16 @@ Create the build folder:
Copy the `x-pack/build-chromium` folder to each. Replace `you@your-machine` with the correct username and VM name:
-- Mac: `cp -r ~/dev/elastic/kibana/x-pack/build_chromium ~/chromium/build_chromium`
-- Linux: `gcloud compute scp --recurse ~/dev/elastic/kibana/x-pack/build_chromium you@your-machine:~/chromium/build_chromium --zone=us-east1-b`
+- Mac: `cp -r x-pack/build_chromium ~/chromium/build_chromium`
+- Linux: `gcloud compute scp --recurse x-pack/build_chromium you@your-machine:~/chromium/ --zone=us-east1-b --project "XXXXXXXX"`
- Windows: Copy the `build_chromium` folder via the RDP GUI into `c:\chromium\build_chromium`
There is an init script for each platform. This downloads and installs the necessary prerequisites, sets environment variables, etc.
-- Mac: `~/chromium/build_chromium/darwin/init.sh`
-- Linux: `~/chromium/build_chromium/linux/init.sh`
-- Windows `c:\chromium\build_chromium\windows\init.bat`
+- Mac x64: `~/chromium/build_chromium/darwin/init.sh`
+- Linux x64: `~/chromium/build_chromium/linux/init.sh`
+- Linux arm64: `~/chromium/build_chromium/linux/init.sh arm64`
+- Windows x64: `c:\chromium\build_chromium\windows\init.bat`
In windows, at least, you will need to do a number of extra steps:
@@ -102,15 +104,16 @@ Note: In Linux, you should run the build command in tmux so that if your ssh ses
To run the build, replace the sha in the following commands with the sha that you wish to build:
-- Mac: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
-- Linux: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
-- Windows: `python c:\chromium\build_chromium\build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Mac x64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Linux x64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Linux arm64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6 arm64`
+- Windows x64: `python c:\chromium\build_chromium\build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
## Artifacts
-After the build completes, there will be a .zip file and a .md5 file in `~/chromium/chromium/src/out/headless`. These are named like so: `chromium-{first_7_of_SHA}-{platform}`, for example: `chromium-4747cc2-linux`.
+After the build completes, there will be a .zip file and a .md5 file in `~/chromium/chromium/src/out/headless`. These are named like so: `chromium-{first_7_of_SHA}-{platform}-{arch}`, for example: `chromium-4747cc2-linux-x64`.
-The zip files need to be deployed to s3. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/plugins/reporting/server/browsers/chromium/paths.ts` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are:
+The zip files need to be deployed to GCP Storage. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/plugins/reporting/server/browsers/chromium/paths.ts` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are:
- `archiveChecksum`: The contents of the `.md5` file, which is the `md5` checksum of the zip file.
- `binaryChecksum`: The `md5` checksum of the `headless_shell` binary itself.
@@ -139,8 +142,8 @@ In the case of Windows, you can use IE to open `http://localhost:9221` and see i
The following links provide helpful context about how the Chromium build works, and its prerequisites:
- https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches
-- https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md
-- https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md
-- https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/windows_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/mac_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/build_instructions.md
- Some build-flag descriptions: https://www.chromium.org/developers/gn-build-configuration
- The serverless Chromium project was indispensable: https://github.com/adieuadieu/serverless-chrome/blob/b29445aa5a96d031be2edd5d1fc8651683bf262c/packages/lambda/builds/chromium/build/build.sh
diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py
index 82b0561fdcfe1..52ba325d6f726 100644
--- a/x-pack/build_chromium/build.py
+++ b/x-pack/build_chromium/build.py
@@ -17,7 +17,10 @@
# 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479
source_version = sys.argv[1]
-print('Building Chromium ' + source_version)
+# Set to "arm" to build for ARM on Linux
+arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'x64'
+
+print('Building Chromium ' + source_version + ' for ' + arch_name)
# Set the environment variables required by the build tools
print('Configuring the build environment')
@@ -42,21 +45,29 @@
print('Copying build args: ' + platform_build_args + ' to out/headless/args.gn')
mkdir('out/headless')
shutil.copyfile(platform_build_args, 'out/headless/args.gn')
+
+print('Adding target_cpu to args')
+
+f = open('out/headless/args.gn', 'a')
+f.write('\rtarget_cpu = "' + arch_name + '"')
+f.close()
+
runcmd('gn gen out/headless')
# Build Chromium... this takes *forever* on underpowered VMs
print('Compiling... this will take a while')
runcmd('autoninja -C out/headless headless_shell')
-# Optimize the output on Linux and Mac by stripping inessentials from the binary
-if platform.system() != 'Windows':
+# Optimize the output on Linux x64 and Mac by stripping inessentials from the binary
+# ARM must be cross-compiled from Linux and can not read the ARM binary in order to strip
+if platform.system() != 'Windows' and arch_name != 'arm64':
print('Optimizing headless_shell')
shutil.move('out/headless/headless_shell', 'out/headless/headless_shell_raw')
runcmd('strip -o out/headless/headless_shell out/headless/headless_shell_raw')
# Create the zip and generate the md5 hash using filenames like:
-# chromium-4747cc2-linux.zip
-base_filename = 'out/headless/chromium-' + source_version[:7].strip('.') + '-' + platform.system().lower()
+# chromium-4747cc2-linux_x64.zip
+base_filename = 'out/headless/chromium-' + source_version[:7].strip('.') + '-' + platform.system().lower() + '_' + arch_name
zip_filename = base_filename + '.zip'
md5_filename = base_filename + '.md5'
@@ -66,7 +77,7 @@
def archive_file(name):
"""A little helper function to write individual files to the zip file"""
from_path = os.path.join('out/headless', name)
- to_path = os.path.join('headless_shell-' + platform.system().lower(), name)
+ to_path = os.path.join('headless_shell-' + platform.system().lower() + '_' + arch_name, name)
archive.write(from_path, to_path)
# Each platform has slightly different requirements for what dependencies
@@ -76,6 +87,9 @@ def archive_file(name):
archive_file(os.path.join('swiftshader', 'libEGL.so'))
archive_file(os.path.join('swiftshader', 'libGLESv2.so'))
+ if arch_name == 'arm64':
+ archive_file(os.path.join('swiftshader', 'libEGL.so'))
+
elif platform.system() == 'Windows':
archive_file('headless_shell.exe')
archive_file('dbghelp.dll')
diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py
index a3c5f8dc16fb7..f543922f7653a 100644
--- a/x-pack/build_chromium/init.py
+++ b/x-pack/build_chromium/init.py
@@ -1,4 +1,4 @@
-import os, platform
+import os, platform, sys
from build_util import runcmd, mkdir, md5_file, root_dir, configure_environment
# This is a cross-platform initialization script which should only be run
@@ -29,4 +29,10 @@
# Build Linux deps
if platform.system() == 'Linux':
os.chdir('src')
+
+ if len(sys.argv) >= 2:
+ sysroot_cmd = 'build/linux/sysroot_scripts/install-sysroot.py --arch=' + sys.argv[1]
+ print('Running `' + sysroot_cmd + '`')
+ runcmd(sysroot_cmd)
+
runcmd('build/install-build-deps.sh')
diff --git a/x-pack/build_chromium/linux/init.sh b/x-pack/build_chromium/linux/init.sh
index e259ebded12a1..83cc4a8e5d4d5 100755
--- a/x-pack/build_chromium/linux/init.sh
+++ b/x-pack/build_chromium/linux/init.sh
@@ -10,4 +10,4 @@ fi
# Launch the cross-platform init script using a relative path
# from this script's location.
-python "`dirname "$0"`/../init.py"
+python "`dirname "$0"`/../init.py" $1
diff --git a/x-pack/dev-tools/jest/setup/polyfills.js b/x-pack/dev-tools/jest/setup/polyfills.js
index 822802f3dacb7..a841a3bf9cad0 100644
--- a/x-pack/dev-tools/jest/setup/polyfills.js
+++ b/x-pack/dev-tools/jest/setup/polyfills.js
@@ -21,3 +21,7 @@ require('whatwg-fetch');
if (!global.URL.hasOwnProperty('createObjectURL')) {
Object.defineProperty(global.URL, 'createObjectURL', { value: () => '' });
}
+
+// Will be replaced with a better solution in EUI
+// https://github.com/elastic/eui/issues/3713
+global._isJest = true;
diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
index a1cd895bb3cd6..160352a9afd66 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json
+++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
@@ -6,5 +6,9 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActionsEnhanced", "data", "discover"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js
index 0118d178f54e5..adccaccecd7da 100644
--- a/x-pack/gulpfile.js
+++ b/x-pack/gulpfile.js
@@ -9,13 +9,11 @@ require('../src/setup_node_env');
const { buildTask } = require('./tasks/build');
const { devTask } = require('./tasks/dev');
const { testTask, testKarmaTask, testKarmaDebugTask } = require('./tasks/test');
-const { prepareTask } = require('./tasks/prepare');
// export the tasks that are runnable from the CLI
module.exports = {
build: buildTask,
dev: devTask,
- prepare: prepareTask,
test: testTask,
'test:karma': testKarmaTask,
'test:karma:debug': testKarmaDebugTask,
diff --git a/x-pack/package.json b/x-pack/package.json
index b721cb2fc563a..29264f8920e5d 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -196,7 +196,7 @@
"@elastic/apm-rum-react": "^1.1.2",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/filesaver": "1.1.2",
"@elastic/maki": "6.3.0",
"@elastic/node-crypto": "1.2.1",
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 494f2f38e8bff..9e07727204f88 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -26,15 +26,19 @@ Table of Contents
- [Executor](#executor)
- [Example](#example)
- [RESTful API](#restful-api)
- - [`POST /api/actions/action`: Create action](#post-apiaction-create-action)
- - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionid-delete-action)
- - [`GET /api/actions`: Get all actions](#get-apiactiongetall-get-all-actions)
- - [`GET /api/actions/action/{id}`: Get action](#get-apiactionid-get-action)
- - [`GET /api/actions/list_action_types`: List action types](#get-apiactiontypes-list-action-types)
- - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionid-update-action)
- - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionidexecute-execute-action)
+ - [`POST /api/actions/action`: Create action](#post-apiactionsaction-create-action)
+ - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionsactionid-delete-action)
+ - [`GET /api/actions`: Get all actions](#get-apiactions-get-all-actions)
+ - [`GET /api/actions/action/{id}`: Get action](#get-apiactionsactionid-get-action)
+ - [`GET /api/actions/list_action_types`: List action types](#get-apiactionslist_action_types-list-action-types)
+ - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionsactionid-update-action)
+ - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionsactionid_execute-execute-action)
- [Firing actions](#firing-actions)
+ - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient)
+ - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions)
- [Example](#example-1)
+ - [actionsClient.execute(options)](#actionsclientexecuteoptions)
+ - [Example](#example-2)
- [Built-in Action Types](#built-in-action-types)
- [Server log](#server-log)
- [`config`](#config)
@@ -70,6 +74,11 @@ Table of Contents
- [`secrets`](#secrets-7)
- [`params`](#params-7)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
+ - [IBM Resilient](#ibm-resilient)
+ - [`config`](#config-8)
+ - [`secrets`](#secrets-8)
+ - [`params`](#params-8)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
@@ -99,7 +108,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba
| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean |
| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array |
| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array |
-| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array |
+| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array |
#### Whitelisting Built-in Action Types
@@ -251,6 +260,7 @@ Once you have a scoped ActionsClient you can execute an action by caling either
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.
@@ -287,14 +297,14 @@ This api runs the action and asynchronously returns the result of running the ac
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 |
+| 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.
+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);
@@ -559,10 +569,10 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
### `config`
-| Property | Description | Type |
-| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
-| apiUrl | ServiceNow instance URL. | string |
-| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object |
+| Property | Description | Type |
+| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
+| apiUrl | Jira instance URL. | string |
+| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object |
### `secrets`
@@ -588,6 +598,41 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+## IBM Resilient
+
+ID: `.resilient`
+
+### `config`
+
+| Property | Description | Type |
+| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
+| apiUrl | IBM Resilient instance URL. | string |
+| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object |
+
+### `secrets`
+
+| Property | Description | Type |
+| ------------ | -------------------------------------------- | ------ |
+| apiKeyId | API key ID for HTTP Basic authentication | string |
+| apiKeySecret | API key secret for HTTP Basic authentication | string |
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------ | ------ |
+| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string |
+| subActionParams | The parameters of the sub action | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| caseId | The case id | string |
+| title | The title of the case | string _(optional)_ |
+| description | The description of the case | string _(optional)_ |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
+| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+
# Command Line Utility
The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command:
diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
index dbb18fa5c695c..2e3cee3946d61 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
@@ -243,7 +243,7 @@ describe('transformFields', () => {
});
});
- test('add newline character to descripton', () => {
+ test('add newline character to description', () => {
const fields = prepareFieldsForTransformation({
externalCase: fullParams.externalCase,
mapping: finalMapping,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts
index 0020161789d71..80a171cbe624d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts
@@ -16,6 +16,7 @@ import { getActionType as getSlackActionType } from './slack';
import { getActionType as getWebhookActionType } from './webhook';
import { getActionType as getServiceNowActionType } from './servicenow';
import { getActionType as getJiraActionType } from './jira';
+import { getActionType as getResilientActionType } from './resilient';
export function registerBuiltInActionTypes({
actionsConfigUtils: configurationUtilities,
@@ -34,4 +35,5 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getJiraActionType({ configurationUtilities }));
+ actionTypeRegistry.register(getResilientActionType({ configurationUtilities }));
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts
new file mode 100644
index 0000000000000..734f6be382629
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts
@@ -0,0 +1,517 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { api } from '../case/api';
+import { externalServiceMock, mapping, apiParams } from './mocks';
+import { ExternalService } from '../case/types';
+
+describe('api', () => {
+ let externalService: jest.Mocked;
+
+ beforeEach(() => {
+ externalService = externalServiceMock.create();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('pushToService', () => {
+ describe('create incident', () => {
+ test('it creates an incident', async () => {
+ const params = { ...apiParams, externalId: null };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ {
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ ],
+ });
+ });
+
+ test('it creates an incident without comments', async () => {
+ const params = { ...apiParams, externalId: null, comments: [] };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it calls createIncident correctly', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping, params });
+
+ expect(externalService.createIncident).toHaveBeenCalledWith({
+ incident: {
+ description:
+ 'Incident description (created at 2020-06-03T15:09:13.606Z by Elastic User)',
+ name: 'Incident title (created at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ expect(externalService.updateIncident).not.toHaveBeenCalled();
+ });
+
+ test('it calls createComment correctly', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping, params });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+ });
+ });
+
+ describe('update incident', () => {
+ test('it updates an incident', async () => {
+ const res = await api.pushToService({ externalService, mapping, params: apiParams });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ {
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ ],
+ });
+ });
+
+ test('it updates an incident without comments', async () => {
+ const params = { ...apiParams, comments: [] };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it calls updateIncident correctly', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping, params });
+
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ expect(externalService.createIncident).not.toHaveBeenCalled();
+ });
+
+ test('it calls createComment correctly', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping, params });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+ });
+ });
+
+ describe('mapping variations', () => {
+ test('overwrite & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {},
+ });
+ });
+
+ test('overwrite & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('overwrite & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('comment nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'nothing',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.createComment).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts
new file mode 100644
index 0000000000000..3db66e5884af4
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.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 { api } from '../case/api';
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts
new file mode 100644
index 0000000000000..4ce9417bfa9a1
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ExternalServiceConfiguration } from '../case/types';
+import * as i18n from './translations';
+
+export const config: ExternalServiceConfiguration = {
+ id: '.resilient',
+ name: i18n.NAME,
+ minimumLicenseRequired: 'platinum',
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts
new file mode 100644
index 0000000000000..e98bc71559d3f
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createConnector } from '../case/utils';
+
+import { api } from './api';
+import { config } from './config';
+import { validate } from './validators';
+import { createExternalService } from './service';
+import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema';
+
+export const getActionType = createConnector({
+ api,
+ config,
+ validate,
+ createExternalService,
+ validationSchema: {
+ config: ResilientPublicConfiguration,
+ secrets: ResilientSecretConfiguration,
+ },
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts
new file mode 100644
index 0000000000000..bba9c58bf28c9
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ ExternalService,
+ PushToServiceApiParams,
+ ExecutorSubActionPushParams,
+ MapRecord,
+} from '../case/types';
+
+const createMock = (): jest.Mocked => {
+ const service = {
+ getIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ name: 'title from ibm resilient',
+ description: 'description from ibm resilient',
+ discovered_date: 1589391874472,
+ create_date: 1591192608323,
+ inc_last_modified_date: 1591192650372,
+ })
+ ),
+ createIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ })
+ ),
+ updateIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ })
+ ),
+ createComment: jest.fn(),
+ };
+
+ service.createComment.mockImplementationOnce(() =>
+ Promise.resolve({
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ externalCommentId: '1',
+ })
+ );
+
+ service.createComment.mockImplementationOnce(() =>
+ Promise.resolve({
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ externalCommentId: '2',
+ })
+ );
+
+ return service;
+};
+
+const externalServiceMock = {
+ create: createMock,
+};
+
+const mapping: Map> = new Map();
+
+mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+});
+
+mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+});
+
+mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+});
+
+mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+});
+
+const executorParams: ExecutorSubActionPushParams = {
+ savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
+ externalId: 'incident-3',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ title: 'Incident title',
+ description: 'Incident description',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ },
+ {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ },
+ ],
+};
+
+const apiParams: PushToServiceApiParams = {
+ ...executorParams,
+ externalCase: { name: 'Incident title', description: 'Incident description' },
+};
+
+export { externalServiceMock, mapping, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
new file mode 100644
index 0000000000000..c13de2b27e2b9
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { ExternalIncidentServiceConfiguration } from '../case/schema';
+
+export const ResilientPublicConfiguration = {
+ orgId: schema.string(),
+ ...ExternalIncidentServiceConfiguration,
+};
+
+export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration);
+
+export const ResilientSecretConfiguration = {
+ apiKeyId: schema.string(),
+ apiKeySecret: schema.string(),
+};
+
+export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts
new file mode 100644
index 0000000000000..573885698014e
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts
@@ -0,0 +1,422 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 axios from 'axios';
+
+import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
+import * as utils from '../lib/axios_utils';
+import { ExternalService } from '../case/types';
+
+jest.mock('axios');
+jest.mock('../lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('../lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const now = Date.now;
+const TIMESTAMP = 1589391874472;
+
+// Incident update makes three calls to the API.
+// The function below mocks this calls.
+// a) Get the latest incident
+// b) Update the incident
+// c) Get the updated incident
+const mockIncidentUpdate = (withUpdateError = false) => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: {
+ format: 'html',
+ content: 'description',
+ },
+ },
+ }));
+
+ if (withUpdateError) {
+ requestMock.mockImplementationOnce(() => {
+ throw new Error('An error has occurred');
+ });
+ } else {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ success: true,
+ id: '1',
+ inc_last_modified_date: 1589391874472,
+ },
+ }));
+ }
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ id: '1',
+ name: 'title_updated',
+ description: {
+ format: 'html',
+ content: 'desc_updated',
+ },
+ inc_last_modified_date: 1589391874472,
+ },
+ }));
+};
+
+describe('IBM Resilient service', () => {
+ let service: ExternalService;
+
+ beforeAll(() => {
+ service = createExternalService({
+ config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' },
+ secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' },
+ });
+ });
+
+ afterAll(() => {
+ Date.now = now;
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ Date.now = jest.fn().mockReturnValue(TIMESTAMP);
+ });
+
+ describe('getValueTextContent', () => {
+ test('transforms correctly', () => {
+ expect(getValueTextContent('name', 'title')).toEqual({
+ text: 'title',
+ });
+ });
+
+ test('transforms correctly the description', () => {
+ expect(getValueTextContent('description', 'desc')).toEqual({
+ textarea: {
+ format: 'html',
+ content: 'desc',
+ },
+ });
+ });
+ });
+
+ describe('formatUpdateRequest', () => {
+ test('transforms correctly', () => {
+ const oldIncident = { name: 'title', description: 'desc' };
+ const newIncident = { name: 'title_updated', description: 'desc_updated' };
+ expect(formatUpdateRequest({ oldIncident, newIncident })).toEqual({
+ changes: [
+ {
+ field: { name: 'name' },
+ old_value: { text: 'title' },
+ new_value: { text: 'title_updated' },
+ },
+ {
+ field: { name: 'description' },
+ old_value: {
+ textarea: {
+ format: 'html',
+ content: 'desc',
+ },
+ },
+ new_value: {
+ textarea: {
+ format: 'html',
+ content: 'desc_updated',
+ },
+ },
+ },
+ ],
+ });
+ });
+ });
+
+ describe('createExternalService', () => {
+ test('throws without url', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: null, orgId: '201' },
+ secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without orgId', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: null },
+ secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without username', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: '201' },
+ secrets: { apiKeyId: '', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without password', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: '201' },
+ secrets: { apiKeyId: '', apiKeySecret: undefined },
+ })
+ ).toThrow();
+ });
+ });
+
+ describe('getIncident', () => {
+ test('it returns the incident correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: '1',
+ description: {
+ format: 'html',
+ content: 'description',
+ },
+ },
+ }));
+ const res = await service.getIncident('1');
+ expect(res).toEqual({ id: '1', name: '1', description: 'description' });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { id: '1' },
+ }));
+
+ await service.getIncident('1');
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
+ params: {
+ text_content_output_format: 'objects_convert',
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ expect(service.getIncident('1')).rejects.toThrow(
+ 'Unable to get incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('createIncident', () => {
+ test('it creates the incident correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: 'description',
+ discovered_date: 1589391874472,
+ create_date: 1589391874472,
+ },
+ }));
+
+ const res = await service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ });
+
+ expect(res).toEqual({
+ title: '1',
+ id: '1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: 'description',
+ discovered_date: 1589391874472,
+ create_date: 1589391874472,
+ },
+ }));
+
+ await service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents',
+ method: 'post',
+ data: {
+ name: 'title',
+ description: {
+ format: 'html',
+ content: 'desc',
+ },
+ discovered_date: TIMESTAMP,
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ expect(
+ service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('updateIncident', () => {
+ test('it updates the incident correctly', async () => {
+ mockIncidentUpdate();
+ const res = await service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title_updated', description: 'desc_updated' },
+ });
+
+ expect(res).toEqual({
+ title: '1',
+ id: '1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ mockIncidentUpdate();
+
+ await service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title_updated', description: 'desc_updated' },
+ });
+
+ // Incident update makes three calls to the API.
+ // The second call to the API is the update call.
+ expect(requestMock.mock.calls[1][0]).toEqual({
+ axios,
+ method: 'patch',
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
+ data: {
+ changes: [
+ {
+ field: { name: 'name' },
+ old_value: { text: 'title' },
+ new_value: { text: 'title_updated' },
+ },
+ {
+ field: { name: 'description' },
+ old_value: {
+ textarea: {
+ content: 'description',
+ format: 'html',
+ },
+ },
+ new_value: {
+ textarea: {
+ content: 'desc_updated',
+ format: 'html',
+ },
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ mockIncidentUpdate(true);
+
+ expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title', description: 'desc' },
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('createComment', () => {
+ test('it creates the comment correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ create_date: 1589391874472,
+ },
+ }));
+
+ const res = await service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'comments',
+ });
+
+ expect(res).toEqual({
+ commentId: 'comment-1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ externalCommentId: '1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ create_date: 1589391874472,
+ },
+ }));
+
+ await service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'my_field',
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ method: 'post',
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments',
+ data: {
+ text: {
+ content: 'comment',
+ format: 'text',
+ },
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ expect(
+ service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'comments',
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts
new file mode 100644
index 0000000000000..8d0526ca3b571
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 axios from 'axios';
+
+import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
+import {
+ ResilientPublicConfigurationType,
+ ResilientSecretConfigurationType,
+ CreateIncidentRequest,
+ UpdateIncidentRequest,
+ CreateCommentRequest,
+ UpdateFieldText,
+ UpdateFieldTextArea,
+} from './types';
+
+import * as i18n from './translations';
+import { getErrorMessage, request } from '../lib/axios_utils';
+
+const BASE_URL = `rest`;
+const INCIDENT_URL = `incidents`;
+const COMMENT_URL = `comments`;
+
+const VIEW_INCIDENT_URL = `#incidents`;
+
+export const getValueTextContent = (
+ field: string,
+ value: string
+): UpdateFieldText | UpdateFieldTextArea => {
+ if (field === 'description') {
+ return {
+ textarea: {
+ format: 'html',
+ content: value,
+ },
+ };
+ }
+
+ return {
+ text: value,
+ };
+};
+
+export const formatUpdateRequest = ({
+ oldIncident,
+ newIncident,
+}: ExternalServiceParams): UpdateIncidentRequest => {
+ return {
+ changes: Object.keys(newIncident).map((key) => ({
+ field: { name: key },
+ old_value: getValueTextContent(key, oldIncident[key]),
+ new_value: getValueTextContent(key, newIncident[key]),
+ })),
+ };
+};
+
+export const createExternalService = ({
+ config,
+ secrets,
+}: ExternalServiceCredentials): ExternalService => {
+ const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType;
+ const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType;
+
+ if (!url || !orgId || !apiKeyId || !apiKeySecret) {
+ throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
+ }
+
+ const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
+ const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/orgs/${orgId}/${INCIDENT_URL}`;
+ const commentUrl = `${incidentUrl}/{inc_id}/${COMMENT_URL}`;
+ const axiosInstance = axios.create({
+ auth: { username: apiKeyId, password: apiKeySecret },
+ });
+
+ const getIncidentViewURL = (key: string) => {
+ return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}/${key}`;
+ };
+
+ const getCommentsURL = (incidentId: string) => {
+ return commentUrl.replace('{inc_id}', incidentId);
+ };
+
+ const getIncident = async (id: string) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ url: `${incidentUrl}/${id}`,
+ params: {
+ text_content_output_format: 'objects_convert',
+ },
+ });
+
+ return { ...res.data, description: res.data.description?.content ?? '' };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`)
+ );
+ }
+ };
+
+ const createIncident = async ({ incident }: ExternalServiceParams) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ url: `${incidentUrl}`,
+ method: 'post',
+ data: {
+ ...incident,
+ description: {
+ format: 'html',
+ content: incident.description ?? '',
+ },
+ discovered_date: Date.now(),
+ },
+ });
+
+ return {
+ title: `${res.data.id}`,
+ id: `${res.data.id}`,
+ pushedDate: new Date(res.data.create_date).toISOString(),
+ url: getIncidentViewURL(res.data.id),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`)
+ );
+ }
+ };
+
+ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
+ try {
+ const latestIncident = await getIncident(incidentId);
+
+ const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident: incident });
+ const res = await request({
+ axios: axiosInstance,
+ method: 'patch',
+ url: `${incidentUrl}/${incidentId}`,
+ data,
+ });
+
+ if (!res.data.success) {
+ throw new Error(res.data.message);
+ }
+
+ const updatedIncident = await getIncident(incidentId);
+
+ return {
+ title: `${updatedIncident.id}`,
+ id: `${updatedIncident.id}`,
+ pushedDate: new Date(updatedIncident.inc_last_modified_date).toISOString(),
+ url: getIncidentViewURL(updatedIncident.id),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to update incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
+ };
+
+ const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'post',
+ url: getCommentsURL(incidentId),
+ data: { text: { format: 'text', content: comment.comment } },
+ });
+
+ return {
+ commentId: comment.commentId,
+ externalCommentId: res.data.id,
+ pushedDate: new Date(res.data.create_date).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
+ };
+
+ return {
+ getIncident,
+ createIncident,
+ updateIncident,
+ createComment,
+ };
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts
new file mode 100644
index 0000000000000..d952838d5a2b3
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/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 NAME = i18n.translate('xpack.actions.builtin.case.resilientTitle', {
+ defaultMessage: 'IBM Resilient',
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts
new file mode 100644
index 0000000000000..6869e2ff3a105
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TypeOf } from '@kbn/config-schema';
+import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema';
+
+export type ResilientPublicConfigurationType = TypeOf;
+export type ResilientSecretConfigurationType = TypeOf;
+
+interface CreateIncidentBasicRequestArgs {
+ name: string;
+ description: string;
+ discovered_date: number;
+}
+
+interface Comment {
+ text: { format: string; content: string };
+}
+
+interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs {
+ comments?: Comment[];
+}
+
+export interface UpdateFieldText {
+ text: string;
+}
+
+export interface UpdateFieldTextArea {
+ textarea: { format: 'html' | 'text'; content: string };
+}
+
+interface UpdateField {
+ field: { name: string };
+ old_value: UpdateFieldText | UpdateFieldTextArea;
+ new_value: UpdateFieldText | UpdateFieldTextArea;
+}
+
+export type CreateIncidentRequest = CreateIncidentRequestArgs;
+export type CreateCommentRequest = Comment;
+
+export interface UpdateIncidentRequest {
+ changes: UpdateField[];
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts
new file mode 100644
index 0000000000000..7226071392bc6
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { validateCommonConfig, validateCommonSecrets } from '../case/validators';
+import { ExternalServiceValidation } from '../case/types';
+
+export const validate: ExternalServiceValidation = {
+ config: validateCommonConfig,
+ secrets: validateCommonSecrets,
+};
diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json
index 56a9e226b6528..ee89abf59ee23 100644
--- a/x-pack/plugins/apm/kibana.json
+++ b/x-pack/plugins/apm/kibana.json
@@ -28,5 +28,10 @@
],
"extraPublicDirs": [
"public/style/variables"
+ ],
+ "requiredBundles": [
+ "kibanaReact",
+ "kibanaUtils",
+ "observability"
]
}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
index a09482d663f65..a173f4068db6a 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
@@ -11,6 +11,12 @@ import { ErrorGroupList } from '../index';
import props from './props.json';
import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider';
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
+ return {
+ htmlIdGenerator: () => () => `generated-id`,
+ };
+});
+
describe('ErrorGroupOverview -> List', () => {
beforeAll(() => {
mockMoment();
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
index 6a20e3c103709..a86f7fdf41f4f 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
@@ -133,6 +133,8 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
List should render with data 1`] = `
List should render with data 1`] = `
-
List should render with data 1`] = `
size="m"
/>
-
-
-
- 1
-
-
-
+
+
+ 1
+
+
+
+
+
List should render with data 1`] = `
size="m"
/>
-
+
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 4ef3d78a7d303..6f985d06dba9d 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -62,7 +62,7 @@ export const AnomalyDetection = () => {
{i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', {
defaultMessage:
- 'The Machine Learning anomaly detection integration enables application health status indicators in the Service map by identifying transaction duration anomalies.',
+ 'The Machine Learning anomaly detection integration enables application health status indicators for each configured environment in the Service map by identifying transaction duration anomalies.',
})}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 674b4492c2c9c..83d19aa27ac11 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -98,7 +98,7 @@ export const JobsList = ({
{i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.addEnvironments',
{
- defaultMessage: 'Add environments',
+ defaultMessage: 'Create ML Job',
}
)}
@@ -108,7 +108,7 @@ export const JobsList = ({
diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/.storybook/storyshots.test.js
index b9fe0914b3698..a3412c3a14e79 100644
--- a/x-pack/plugins/canvas/.storybook/storyshots.test.js
+++ b/x-pack/plugins/canvas/.storybook/storyshots.test.js
@@ -63,6 +63,14 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
};
});
+// To be resolved by EUI team.
+// https://github.com/elastic/eui/issues/3712
+jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => {
+ return {
+ EuiOverlayMask: ({children}) => children,
+ };
+});
+
// Disabling this test due to https://github.com/elastic/eui/issues/2242
jest.mock(
'../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories',
diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json
index 2d6ab43228aa1..5f4ea5802cb13 100644
--- a/x-pack/plugins/canvas/kibana.json
+++ b/x-pack/plugins/canvas/kibana.json
@@ -6,5 +6,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "embeddable", "expressions", "features", "home", "inspector", "uiActions"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting"]
}
diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
index 8afa5d16b59fd..7dc8b762359f9 100644
--- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
+++ b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
@@ -15,7 +15,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel, keyCodes } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, keys } from '@elastic/eui';
/**
* An autocomplete component. Currently this is only used for the expression editor but in theory
@@ -134,27 +134,27 @@ export class Autocomplete extends React.Component {
* the item selection, closing the menu, etc.
*/
onKeyDown = (e) => {
- const { ESCAPE, TAB, ENTER, UP, DOWN, LEFT, RIGHT } = keyCodes;
- const { keyCode } = e;
+ const { BACKSPACE, ESCAPE, TAB, ENTER, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } = keys;
+ const { key } = e;
const { items } = this.props;
const { isOpen, selectedIndex } = this.state;
- if ([ESCAPE, LEFT, RIGHT].includes(keyCode)) {
+ if ([ESCAPE, ARROW_LEFT, ARROW_RIGHT].includes(key)) {
this.setState({ isOpen: false });
}
- if ([TAB, ENTER].includes(keyCode) && isOpen && selectedIndex >= 0) {
+ if ([TAB, ENTER].includes(key) && isOpen && selectedIndex >= 0) {
e.preventDefault();
this.onSubmit();
- } else if (keyCode === UP && items.length > 0 && isOpen) {
+ } else if (key === ARROW_UP && items.length > 0 && isOpen) {
e.preventDefault();
this.selectPrevious();
- } else if (keyCode === DOWN && items.length > 0 && isOpen) {
+ } else if (key === ARROW_DOWN && items.length > 0 && isOpen) {
e.preventDefault();
this.selectNext();
- } else if (e.key === 'Backspace') {
+ } else if (key === BACKSPACE) {
this.setState({ isOpen: true });
- } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) {
+ } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(key)) {
this.setState({ selectedIndex: -1 });
}
};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
index ea31d1daa97ca..97d13dcd69830 100644
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
@@ -156,6 +156,8 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = `
-
-
-
-
- 1
-
-
-
+
+
+ 1
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts
index 7d20011a428cf..38fff5b190f25 100644
--- a/x-pack/plugins/case/common/api/cases/configure.ts
+++ b/x-pack/plugins/case/common/api/cases/configure.ts
@@ -10,6 +10,7 @@ import { ActionResult } from '../../../../actions/common';
import { UserRT } from '../user';
import { JiraFieldsRT } from '../connectors/jira';
import { ServiceNowFieldsRT } from '../connectors/servicenow';
+import { ResilientFieldsRT } from '../connectors/resilient';
/*
* This types below are related to the service now configuration
@@ -29,7 +30,12 @@ const CaseFieldRT = rt.union([
rt.literal('comments'),
]);
-const ThirdPartyFieldRT = rt.union([JiraFieldsRT, ServiceNowFieldsRT, rt.literal('not_mapped')]);
+const ThirdPartyFieldRT = rt.union([
+ JiraFieldsRT,
+ ServiceNowFieldsRT,
+ ResilientFieldsRT,
+ rt.literal('not_mapped'),
+]);
export const CasesConfigurationMapsRT = rt.type({
source: CaseFieldRT,
diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts
index c1fc284c938b7..0a7840d3aba22 100644
--- a/x-pack/plugins/case/common/api/connectors/index.ts
+++ b/x-pack/plugins/case/common/api/connectors/index.ts
@@ -6,3 +6,4 @@
export * from './jira';
export * from './servicenow';
+export * from './resilient';
diff --git a/x-pack/plugins/case/common/api/connectors/resilient.ts b/x-pack/plugins/case/common/api/connectors/resilient.ts
new file mode 100644
index 0000000000000..c7e2f19809140
--- /dev/null
+++ b/x-pack/plugins/case/common/api/connectors/resilient.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as rt from 'io-ts';
+
+export const ResilientFieldsRT = rt.union([
+ rt.literal('name'),
+ rt.literal('description'),
+ rt.literal('comments'),
+]);
+
+export type ResilientFieldsType = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index e912c661439b2..bd12c258a5388 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -29,4 +29,4 @@ export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
-export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira'];
+export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient'];
diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json
index ccf98f41def47..13746bb0e34c3 100644
--- a/x-pack/plugins/cross_cluster_replication/kibana.json
+++ b/x-pack/plugins/cross_cluster_replication/kibana.json
@@ -13,5 +13,10 @@
"optionalPlugins": [
"usageCollection"
],
- "configPath": ["xpack", "ccr"]
+ "configPath": ["xpack", "ccr"],
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared",
+ "data"
+ ]
}
diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json
index 3a95419d2f2fe..ba5d8052ca787 100644
--- a/x-pack/plugins/dashboard_enhanced/kibana.json
+++ b/x-pack/plugins/dashboard_enhanced/kibana.json
@@ -4,5 +4,10 @@
"server": false,
"ui": true,
"requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"],
- "configPath": ["xpack", "dashboardEnhanced"]
+ "configPath": ["xpack", "dashboardEnhanced"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "embeddableEnhanced",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json
index 1be55d2b7a635..f0baa84afca32 100644
--- a/x-pack/plugins/data_enhanced/kibana.json
+++ b/x-pack/plugins/data_enhanced/kibana.json
@@ -10,5 +10,6 @@
],
"optionalPlugins": ["kibanaReact", "kibanaUtils"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaReact", "kibanaUtils"]
}
diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json
index 704096ce7fcad..fbd04fe009687 100644
--- a/x-pack/plugins/discover_enhanced/kibana.json
+++ b/x-pack/plugins/discover_enhanced/kibana.json
@@ -6,5 +6,6 @@
"ui": true,
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share"],
- "configPath": ["xpack", "discoverEnhanced"]
+ "configPath": ["xpack", "discoverEnhanced"],
+ "requiredBundles": ["kibanaUtils", "data"]
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
index 0e1bb796cbf2e..8f72875a32bd4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Breadcrumb as EuiBreadcrumb } from '@elastic/eui';
+import { EuiBreadcrumb } from '@elastic/eui';
import { History } from 'history';
import { letBrowserHandleEvent } from '../react_router_helpers';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
index ad3cd65c09516..530117e197616 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
@@ -6,7 +6,7 @@
import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
-import { Breadcrumb as EuiBreadcrumb } from '@elastic/eui';
+import { EuiBreadcrumb } from '@elastic/eui';
import { KibanaContext, IKibanaContext } from '../../index';
import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs';
diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json
index ebe18dba2b58c..4e653393100c9 100644
--- a/x-pack/plugins/graph/kibana.json
+++ b/x-pack/plugins/graph/kibana.json
@@ -6,5 +6,6 @@
"ui": true,
"requiredPlugins": ["licensing", "data", "navigation", "savedObjects", "kibanaLegacy"],
"optionalPlugins": ["home", "features"],
- "configPath": ["xpack", "graph"]
+ "configPath": ["xpack", "graph"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "home"]
}
diff --git a/x-pack/plugins/grokdebugger/kibana.json b/x-pack/plugins/grokdebugger/kibana.json
index 4d37f9ccdb0de..8466c191ed9b6 100644
--- a/x-pack/plugins/grokdebugger/kibana.json
+++ b/x-pack/plugins/grokdebugger/kibana.json
@@ -9,5 +9,8 @@
],
"server": true,
"ui": true,
- "configPath": ["xpack", "grokdebugger"]
+ "configPath": ["xpack", "grokdebugger"],
+ "requiredBundles": [
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json
index 6385646b95789..1a9f133b846fb 100644
--- a/x-pack/plugins/index_lifecycle_management/kibana.json
+++ b/x-pack/plugins/index_lifecycle_management/kibana.json
@@ -12,5 +12,10 @@
"usageCollection",
"indexManagement"
],
- "configPath": ["xpack", "ilm"]
+ "configPath": ["xpack", "ilm"],
+ "requiredBundles": [
+ "indexManagement",
+ "kibanaReact",
+ "esUiShared"
+ ]
}
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
index dfcbb51869466..89a95135bb07a 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
@@ -127,8 +127,8 @@ describe('Data Streams tab', () => {
const { tableCellsValues } = table.getMetaData('dataStreamTable');
expect(tableCellsValues).toEqual([
- ['', 'dataStream1', '1', ''],
- ['', 'dataStream2', '1', ''],
+ ['', 'dataStream1', '1', 'Delete'],
+ ['', 'dataStream2', '1', 'Delete'],
]);
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index 276101486aa61..f7ebc0bcf632b 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -139,7 +139,7 @@ describe('Index Templates tab', () => {
ilmPolicyName,
composedOfString,
hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
- '', // Column of actions
+ 'EditDelete', // Column of actions
]);
} catch (e) {
console.error(`Error in index template at row ${i}`); // eslint-disable-line no-console
@@ -162,7 +162,7 @@ describe('Index Templates tab', () => {
indexPatterns.join(', '),
ilmPolicyName,
hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
- '', // Column of actions
+ 'EditDelete', // Column of actions
]);
} catch (e) {
console.error(`Error in legacy template at row ${i}`); // eslint-disable-line no-console
diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
index 8e8c2632a2372..49902d8b09675 100644
--- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
@@ -198,18 +198,10 @@ describe('index table', () => {
});
test('should show system indices only when the switch is turned on', () => {
const rendered = mountWithIntl(component);
- snapshot(
- rendered
- .find('.euiPagination .euiPaginationButton .euiButtonEmpty__content > span')
- .map((span) => span.text())
- );
+ snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
const switchControl = rendered.find('.euiSwitch__button');
switchControl.simulate('click');
- snapshot(
- rendered
- .find('.euiPagination .euiPaginationButton .euiButtonEmpty__content > span')
- .map((span) => span.text())
- );
+ snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
});
test('should filter based on content of search input', () => {
const rendered = mountWithIntl(component);
diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json
index 40ecb26e8f0c9..6ab691054382e 100644
--- a/x-pack/plugins/index_management/kibana.json
+++ b/x-pack/plugins/index_management/kibana.json
@@ -13,5 +13,9 @@
"usageCollection",
"ingestManager"
],
- "configPath": ["xpack", "index_management"]
+ "configPath": ["xpack", "index_management"],
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared"
+ ]
}
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 86eb88017b77f..6f09e51255f3b 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
@@ -65,7 +65,7 @@ describe(' ', () => {
const { name, usedBy } = componentTemplates[i];
const usedByText = usedBy.length === 0 ? 'Not in use' : usedBy.length.toString();
- expect(row).toEqual(['', name, usedByText, '', '', '', '']);
+ expect(row).toEqual(['', name, usedByText, '', '', '', 'EditDelete']);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
index 2777941175429..ad98aee5fb5f1 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
@@ -62,20 +62,21 @@ function getFieldsMeta(esDocsBase: string) {
description: (
-
-
- {i18n.translate(
- 'xpack.idxMgmt.templateForm.stepLogistics.dataStreamDocumentionLink',
- {
- defaultMessage: 'Learn more about data streams.',
- }
- )}
-
- >
+
+ {i18n.translate(
+ 'xpack.idxMgmt.templateForm.stepLogistics.dataStreamDocumentionLink',
+ {
+ defaultMessage: 'Learn more about data streams.',
+ }
+ )}
+
),
}}
/>
diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json
index e5ce1b1cd96f8..06394c2aa916c 100644
--- a/x-pack/plugins/infra/kibana.json
+++ b/x-pack/plugins/infra/kibana.json
@@ -16,5 +16,12 @@
"optionalPlugins": ["ml", "observability"],
"server": true,
"ui": true,
- "configPath": ["xpack", "infra"]
+ "configPath": ["xpack", "infra"],
+ "requiredBundles": [
+ "observability",
+ "licenseManagement",
+ "kibanaUtils",
+ "kibanaReact",
+ "apm"
+ ]
}
diff --git a/x-pack/plugins/infra/public/utils/datemath.test.ts b/x-pack/plugins/infra/public/utils/datemath.test.ts
index c8fbe5583db2e..e073afb231b0b 100644
--- a/x-pack/plugins/infra/public/utils/datemath.test.ts
+++ b/x-pack/plugins/infra/public/utils/datemath.test.ts
@@ -196,6 +196,15 @@ describe('extendDatemath()', () => {
diffUnit: 'y',
});
});
+
+ it('Returns no difference if the next value would result in an epoch smaller than 0', () => {
+ // FIXME: Test will fail in ~551 years
+ expect(extendDatemath('now-500y', 'before')).toBeUndefined();
+
+ expect(
+ extendDatemath('1970-01-01T00:00:00.000Z', 'before', '1970-01-01T00:00:00.001Z')
+ ).toBeUndefined();
+ });
});
describe('with a positive operator', () => {
@@ -573,6 +582,13 @@ describe('extendDatemath()', () => {
diffUnit: 'y',
});
});
+
+ it('Returns no difference if the next value would result in an epoch bigger than the max JS date', () => {
+ expect(extendDatemath('now+275760y', 'after')).toBeUndefined();
+ expect(
+ extendDatemath('+275760-09-13T00:00:00.000Z', 'after', '+275760-09-12T23:59:59.999Z')
+ ).toBeUndefined();
+ });
});
});
});
diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts
index f2bd5d94ac2c3..791fe4bdb8da7 100644
--- a/x-pack/plugins/infra/public/utils/datemath.ts
+++ b/x-pack/plugins/infra/public/utils/datemath.ts
@@ -6,6 +6,8 @@
import dateMath, { Unit } from '@elastic/datemath';
+const JS_MAX_DATE = 8640000000000000;
+
export function isValidDatemath(value: string): boolean {
const parsedValue = dateMath.parse(value);
return !!(parsedValue && parsedValue.isValid());
@@ -136,18 +138,24 @@ function extendRelativeDatemath(
// if `diffAmount` is not an integer after normalization, express the difference in the original unit
const shouldKeepDiffUnit = diffAmount % 1 !== 0;
- return {
- value: `now${operator}${normalizedAmount}${normalizedUnit}`,
- diffUnit: shouldKeepDiffUnit ? unit : newUnit,
- diffAmount: shouldKeepDiffUnit ? Math.abs(newAmount - parsedAmount) : diffAmount,
- };
+ const nextValue = `now${operator}${normalizedAmount}${normalizedUnit}`;
+
+ if (isDateInRange(nextValue)) {
+ return {
+ value: nextValue,
+ diffUnit: shouldKeepDiffUnit ? unit : newUnit,
+ diffAmount: shouldKeepDiffUnit ? Math.abs(newAmount - parsedAmount) : diffAmount,
+ };
+ } else {
+ return undefined;
+ }
}
function extendAbsoluteDatemath(
value: string,
direction: 'before' | 'after',
oppositeEdge: string
-): DatemathExtension {
+): DatemathExtension | undefined {
const valueTimestamp = datemathToEpochMillis(value)!;
const oppositeEdgeTimestamp = datemathToEpochMillis(oppositeEdge)!;
const actualTimestampDiff = Math.abs(valueTimestamp - oppositeEdgeTimestamp);
@@ -159,11 +167,15 @@ function extendAbsoluteDatemath(
? valueTimestamp - normalizedTimestampDiff
: valueTimestamp + normalizedTimestampDiff;
- return {
- value: new Date(newValue).toISOString(),
- diffUnit: normalizedDiff.unit,
- diffAmount: normalizedDiff.amount,
- };
+ if (isDateInRange(newValue)) {
+ return {
+ value: new Date(newValue).toISOString(),
+ diffUnit: normalizedDiff.unit,
+ diffAmount: normalizedDiff.amount,
+ };
+ } else {
+ return undefined;
+ }
}
const CONVERSION_RATIOS: Record> = {
@@ -265,3 +277,12 @@ export function normalizeDate(amount: number, unit: Unit): { amount: number; uni
// Cannot go one one unit above. Return as it is
return { amount, unit };
}
+
+function isDateInRange(date: string | number): boolean {
+ try {
+ const epoch = typeof date === 'string' ? datemathToEpochMillis(date) ?? -1 : date;
+ return epoch >= 0 && epoch <= JS_MAX_DATE;
+ } catch {
+ return false;
+ }
+}
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
index de5eda4a1f2c3..7f6bf9551e2c1 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
@@ -23,6 +23,7 @@ interface Aggregation {
buckets: Array<{
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
doc_count: number;
+ key_as_string: string;
}>;
};
}
@@ -57,17 +58,18 @@ export const evaluateAlert = (
);
const { threshold, comparator } = criterion;
const comparisonFunction = comparatorMap[comparator];
- return mapValues(currentValues, (values: number | number[] | null) => {
- if (isTooManyBucketsPreviewException(values)) throw values;
+ return mapValues(currentValues, (points: any[] | typeof NaN | null) => {
+ if (isTooManyBucketsPreviewException(points)) throw points;
return {
...criterion,
metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
- currentValue: Array.isArray(values) ? last(values) : NaN,
- shouldFire: Array.isArray(values)
- ? values.map((value) => comparisonFunction(value, threshold))
+ currentValue: Array.isArray(points) ? last(points)?.value : NaN,
+ timestamp: Array.isArray(points) ? last(points)?.key : NaN,
+ shouldFire: Array.isArray(points)
+ ? points.map((point) => comparisonFunction(point.value, threshold))
: [false],
- isNoData: values === null,
- isError: isNaN(values),
+ isNoData: points === null,
+ isError: isNaN(points),
};
});
})
@@ -157,17 +159,20 @@ const getValuesFromAggregations = (
const { buckets } = aggregations.aggregatedIntervals;
if (!buckets.length) return null; // No Data state
if (aggType === Aggregators.COUNT) {
- return buckets.map((bucket) => bucket.doc_count);
+ return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count }));
}
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
return buckets.map((bucket) => {
const values = bucket.aggregatedValue?.values || [];
const firstValue = first(values);
if (!firstValue) return null;
- return firstValue.value;
+ return { key: bucket.key_as_string, value: firstValue.value };
});
}
- return buckets.map((bucket) => bucket.aggregatedValue.value);
+ return buckets.map((bucket) => ({
+ key: bucket.key_as_string,
+ value: bucket.aggregatedValue.value,
+ }));
} catch (e) {
return NaN; // Error state
}
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
index 3ad1031f574e2..b4fe8f053a44a 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
@@ -56,4 +56,26 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
);
});
});
+
+ describe('handles time', () => {
+ const end = new Date('2020-07-08T22:07:27.235Z').valueOf();
+ const timerange = {
+ end,
+ start: end - 5 * 60 * 1000,
+ };
+ const searchBody = getElasticsearchMetricQuery(
+ expressionParams,
+ timefield,
+ undefined,
+ undefined,
+ timerange
+ );
+ test('by rounding timestamps to the nearest timeUnit', () => {
+ const rangeFilter = searchBody.query.bool.filter.find((filter) =>
+ filter.hasOwnProperty('range')
+ )?.range[timefield];
+ expect(rangeFilter?.lte).toBe(1594246020000);
+ expect(rangeFilter?.gte).toBe(1594245720000);
+ });
+ });
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
index 15506a30529c4..078ca46d42e60 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
@@ -3,9 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
import { MetricExpressionParams, Aggregators } from '../types';
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
+import { roundTimestamp } from '../../../../utils/round_timestamp';
import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
import { createPercentileAggregation } from './create_percentile_aggregation';
@@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = (
const interval = `${timeSize}${timeUnit}`;
const intervalAsSeconds = getIntervalInSeconds(interval);
- const to = timeframe ? timeframe.end : Date.now();
+ const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit);
// We need enough data for 5 buckets worth of data. We also need
// to convert the intervalAsSeconds to milliseconds.
const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS;
- const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom;
+ const from = roundTimestamp(
+ timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom,
+ timeUnit
+ );
const offset = getDateHistogramOffset(from, interval);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index 24f4bc2c678b4..003a6c3c20e98 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => {
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
});
test('reports expected values to the action context', async () => {
+ const now = 1577858400000;
await execute(Comparator.GT, [0.75]);
const { action } = mostRecentAction(instanceID);
expect(action.group).toBe('*');
expect(action.reason).toContain('current value is 1');
expect(action.reason).toContain('threshold of 0.75');
expect(action.reason).toContain('test.metric.1');
+ expect(action.timestamp).toBe(new Date(now).toISOString());
});
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 4c02593dd0095..bc1cc24f65eeb 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
}
}
if (reason) {
+ const firstResult = first(alertResults);
+ const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString();
alertInstance.scheduleActions(FIRED_ACTIONS.id, {
group,
alertState: stateToAlertMessage[nextState],
reason,
- timestamp: moment().toISOString(),
+ timestamp,
value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
index ee2cf94a2fd62..c7e53eb2008f5 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
@@ -12,6 +12,7 @@ const bucketsA = [
{
doc_count: 3,
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
+ key_as_string: new Date(1577858400000).toISOString(),
},
];
diff --git a/x-pack/plugins/infra/server/utils/round_timestamp.ts b/x-pack/plugins/infra/server/utils/round_timestamp.ts
new file mode 100644
index 0000000000000..9b5ae2ac40197
--- /dev/null
+++ b/x-pack/plugins/infra/server/utils/round_timestamp.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Unit } from '@elastic/datemath';
+import moment from 'moment';
+
+export const roundTimestamp = (timestamp: number, unit: Unit) => {
+ const floor = moment(timestamp).startOf(unit).valueOf();
+ const ceil = moment(timestamp).add(1, unit).startOf(unit).valueOf();
+ if (Math.abs(timestamp - floor) <= Math.abs(timestamp - ceil)) return floor;
+ return ceil;
+};
diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts
new file mode 100644
index 0000000000000..131917af44595
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/common/mocks.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { NewPackageConfig, PackageConfig } from './types/models/package_config';
+
+export const createNewPackageConfigMock = () => {
+ return {
+ name: 'endpoint-1',
+ description: '',
+ namespace: 'default',
+ enabled: true,
+ config_id: '93c46720-c217-11ea-9906-b5b8a21b268e',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.9.0',
+ },
+ inputs: [],
+ } as NewPackageConfig;
+};
+
+export const createPackageConfigMock = () => {
+ const newPackageConfig = createNewPackageConfigMock();
+ return {
+ ...newPackageConfig,
+ id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1',
+ version: 'abcd',
+ revision: 1,
+ updated_at: '2020-06-25T16:03:38.159292',
+ updated_by: 'kibana',
+ created_at: '2020-06-25T16:03:38.159292',
+ created_by: 'kibana',
+ inputs: [
+ {
+ config: {},
+ },
+ ],
+ } as PackageConfig;
+};
diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json
index 877184740166f..ab0a2ba24ba66 100644
--- a/x-pack/plugins/ingest_manager/kibana.json
+++ b/x-pack/plugins/ingest_manager/kibana.json
@@ -6,5 +6,6 @@
"configPath": ["xpack", "ingestManager"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects"],
"optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"],
- "extraPublicDirs": ["common"]
+ "extraPublicDirs": ["common"],
+ "requiredBundles": ["kibanaReact", "esUiShared"]
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
index 5ad558dfafe7d..c732bc349687d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
index 3e0b78d4f2e9d..8d6a83a625651 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
@@ -72,7 +72,7 @@ describe(' ', () => {
tableCellsValues.forEach((row, i) => {
const pipeline = pipelines[i];
- expect(row).toEqual(['', pipeline.name, '']);
+ expect(row).toEqual(['', pipeline.name, 'EditDelete']);
});
});
diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json
index cb24133b1f6ba..75e5e9b5d6c51 100644
--- a/x-pack/plugins/ingest_pipelines/kibana.json
+++ b/x-pack/plugins/ingest_pipelines/kibana.json
@@ -5,5 +5,6 @@
"ui": true,
"requiredPlugins": ["licensing", "management"],
"optionalPlugins": ["security", "usageCollection"],
- "configPath": ["xpack", "ingest_pipelines"]
+ "configPath": ["xpack", "ingest_pipelines"],
+ "requiredBundles": ["esUiShared", "kibanaReact"]
}
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
index 00ac8d4f6d729..ea936115f1ac9 100644
--- 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
@@ -5,7 +5,7 @@
*/
import classNames from 'classnames';
import React, { FunctionComponent, useState, useEffect, useCallback } from 'react';
-import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui';
+import { EuiFieldText, EuiText, keys } from '@elastic/eui';
export interface Props {
placeholder: string;
@@ -40,10 +40,10 @@ export const InlineTextInput: FunctionComponent = ({
useEffect(() => {
const keyboardListener = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') {
+ if (event.key === keys.ESCAPE || event.code === 'Escape') {
setIsShowingTextInput(false);
}
- if (event.keyCode === keyCodes.ENTER || event.code === 'Enter') {
+ if (event.key === keys.ENTER || event.code === 'Enter') {
submitChange();
}
};
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
index db71cf25faacc..4458bd66c88de 100644
--- 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
@@ -4,7 +4,7 @@
* 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 { EuiFlexGroup, EuiFlexItem, keys } from '@elastic/eui';
import { List, WindowScroller } from 'react-virtualized';
import { ProcessorInternal, ProcessorSelector } from '../../types';
@@ -52,7 +52,7 @@ export const ProcessorsTree: FunctionComponent = memo((props) => {
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') {
+ if (event.key === keys.ESCAPE || event.code === 'Escape') {
onAction({ type: 'cancelMove' });
}
};
diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json
index 7da5eaed5155e..b8747fc1f0cde 100644
--- a/x-pack/plugins/lens/kibana.json
+++ b/x-pack/plugins/lens/kibana.json
@@ -15,5 +15,6 @@
],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"],
"configPath": ["xpack", "lens"],
- "extraPublicDirs": ["common/constants"]
+ "extraPublicDirs": ["common/constants"],
+ "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"]
}
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index cc8cbfe679eff..f0feb826f956d 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -294,7 +294,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
{
diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts
index ffd8afd54913f..fef7f19f02df2 100644
--- a/x-pack/plugins/lists/server/routes/init_routes.ts
+++ b/x-pack/plugins/lists/server/routes/init_routes.ts
@@ -6,8 +6,11 @@
import { IRouter } from 'kibana/server';
+import { SecurityPluginSetup } from '../../../security/server';
import { ConfigType } from '../config';
+import { readPrivilegesRoute } from './read_privileges_route';
+
import {
createExceptionListItemRoute,
createExceptionListRoute,
@@ -38,7 +41,11 @@ import {
updateListRoute,
} from '.';
-export const initRoutes = (router: IRouter, config: ConfigType): void => {
+export const initRoutes = (
+ router: IRouter,
+ config: ConfigType,
+ security: SecurityPluginSetup | null | undefined
+): void => {
// lists
createListRoute(router);
readListRoute(router);
@@ -46,6 +53,7 @@ export const initRoutes = (router: IRouter, config: ConfigType): void => {
deleteListRoute(router);
patchListRoute(router);
findListRoute(router);
+ readPrivilegesRoute(router, security);
// list items
createListItemRoute(router);
diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.ts
new file mode 100644
index 0000000000000..892b6406a28ec
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/read_privileges_route.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouter } from 'kibana/server';
+import { merge } from 'lodash/fp';
+
+import { SecurityPluginSetup } from '../../../security/server';
+import { LIST_PRIVILEGES_URL } from '../../common/constants';
+import { buildSiemResponse, readPrivileges, transformError } from '../siem_server_deps';
+
+import { getListClient } from './utils';
+
+export const readPrivilegesRoute = (
+ router: IRouter,
+ security: SecurityPluginSetup | null | undefined
+): void => {
+ router.get(
+ {
+ options: {
+ tags: ['access:lists'],
+ },
+ path: LIST_PRIVILEGES_URL,
+ validate: false,
+ },
+ async (context, request, response) => {
+ const siemResponse = buildSiemResponse(response);
+ try {
+ const clusterClient = context.core.elasticsearch.legacy.client;
+ const lists = getListClient(context);
+ const clusterPrivilegesLists = await readPrivileges(
+ clusterClient.callAsCurrentUser,
+ lists.getListIndex()
+ );
+ const clusterPrivilegesListItems = await readPrivileges(
+ clusterClient.callAsCurrentUser,
+ lists.getListIndex()
+ );
+ const privileges = merge(
+ {
+ listItems: clusterPrivilegesListItems,
+ lists: clusterPrivilegesLists,
+ },
+ {
+ is_authenticated: security?.authc.isAuthenticated(request) ?? false,
+ }
+ );
+ return response.ok({ body: privileges });
+ } catch (err) {
+ const error = transformError(err);
+ return siemResponse.error({
+ body: error.message,
+ statusCode: error.statusCode,
+ });
+ }
+ }
+ );
+};
diff --git a/x-pack/plugins/lists/server/scripts/get_privileges.sh b/x-pack/plugins/lists/server/scripts/get_privileges.sh
new file mode 100755
index 0000000000000..4c02747f3c56c
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/get_privileges.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+# Example: ./get_privileges.sh
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET ${KIBANA_URL}${SPACE_URL}/api/lists/privileges | jq .
diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts
index 87a623c7a1892..324103b7fb50d 100644
--- a/x-pack/plugins/lists/server/siem_server_deps.ts
+++ b/x-pack/plugins/lists/server/siem_server_deps.ts
@@ -17,4 +17,5 @@ export {
createBootstrapIndex,
getIndexExists,
buildRouteValidation,
+ readPrivileges,
} from '../../security_solution/server';
diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json
index 1eb325dcc1610..5949d5db041f2 100644
--- a/x-pack/plugins/logstash/kibana.json
+++ b/x-pack/plugins/logstash/kibana.json
@@ -13,5 +13,6 @@
"security"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["home"]
}
diff --git a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
index b37131d80168e..5f54513c427dd 100644
--- a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
+++ b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
@@ -159,7 +159,9 @@ exports[`UpgradeFailure component passes expected text for new pipeline 1`] = `
-
+
-
+
-
+
{
if (isKeyboardEvent(e)) {
- if (e.keyCode === keyCodes.ENTER) {
+ if (e.key === keys.ENTER) {
e.preventDefault();
this._togglePopover();
- } else if (e.keyCode === keyCodes.DOWN) {
+ } else if (e.key === keys.ARROW_DOWN) {
this._openPopover();
}
}
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index f93e7bc19f960..a08b9b6d97116 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -25,5 +25,13 @@
"licenseManagement"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "esUiShared",
+ "kibanaUtils",
+ "kibanaReact",
+ "management",
+ "dashboard",
+ "savedObjects"
+ ]
}
diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
index 056fd04857cba..1b33d68042295 100644
--- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
+++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
@@ -62,7 +62,7 @@ describe('FieldTitleBar', () => {
expect(hasClassName).toBeTruthy();
});
- test(`tooltip hovering`, () => {
+ test(`tooltip hovering`, (done) => {
const props = { card: { fieldName: 'foo', type: 'bar' } };
const wrapper = mountWithIntl( );
const container = wrapper.find({ className: 'field-name' });
@@ -70,9 +70,14 @@ describe('FieldTitleBar', () => {
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover');
- expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
-
- container.simulate('mouseout');
- expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
+ // EuiToolTip mounts children after a 250ms delay
+ setTimeout(() => {
+ wrapper.update();
+ expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
+ container.simulate('mouseout');
+ wrapper.update();
+ expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
+ done();
+ }, 250);
});
});
diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
index f616f7cb1b866..7e37dc10ade33 100644
--- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
+++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
@@ -35,7 +35,8 @@ describe('FieldTypeIcon', () => {
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover');
- expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
+ // EuiToolTip mounts children after a 250ms delay
+ setTimeout(() => expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2), 250);
container.simulate('mouseout');
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
diff --git a/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts b/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
index b85fb634891e5..05b941f2544b4 100644
--- a/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
+++ b/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
@@ -48,7 +48,7 @@ type BUTTON_ICON_COLORS = any;
type ButtonIconColorsFunc = (item: T) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS)
interface DefaultItemActionType {
type?: 'icon' | 'button';
- name: string;
+ name: ReactNode;
description: string;
onClick?(item: T): void;
href?: string;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
index def6acdae14e3..ff8797bc523c1 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
@@ -85,16 +85,27 @@ export const AnalysisFieldsTable: FC<{
includes: string[];
loadingItems: boolean;
setFormState: React.Dispatch>;
+ minimumFieldsRequiredMessage?: string;
+ setMinimumFieldsRequiredMessage: React.Dispatch>;
tableItems: FieldSelectionItem[];
-}> = ({ dependentVariable, includes, loadingItems, setFormState, tableItems }) => {
+ unsupportedFieldsError?: string;
+ setUnsupportedFieldsError: React.Dispatch>;
+}> = ({
+ dependentVariable,
+ includes,
+ loadingItems,
+ setFormState,
+ minimumFieldsRequiredMessage,
+ setMinimumFieldsRequiredMessage,
+ tableItems,
+ unsupportedFieldsError,
+ setUnsupportedFieldsError,
+}) => {
const [sortableProperties, setSortableProperties] = useState();
const [currentPaginationData, setCurrentPaginationData] = useState<{
pageIndex: number;
itemsPerPage: number;
}>({ pageIndex: 0, itemsPerPage: 5 });
- const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState<
- undefined | string
- >(undefined);
useEffect(() => {
if (includes.length === 0 && tableItems.length > 0) {
@@ -164,8 +175,21 @@ export const AnalysisFieldsTable: FC<{
label={i18n.translate('xpack.ml.dataframe.analytics.create.includedFieldsLabel', {
defaultMessage: 'Included fields',
})}
- isInvalid={minimumFieldsRequiredMessage !== undefined}
- error={minimumFieldsRequiredMessage}
+ fullWidth
+ isInvalid={
+ minimumFieldsRequiredMessage !== undefined || unsupportedFieldsError !== undefined
+ }
+ error={[
+ ...(minimumFieldsRequiredMessage !== undefined ? [minimumFieldsRequiredMessage] : []),
+ ...(unsupportedFieldsError !== undefined
+ ? [
+ i18n.translate('xpack.ml.dataframe.analytics.create.unsupportedFieldsError', {
+ defaultMessage: 'Invalid. {message}',
+ values: { message: unsupportedFieldsError },
+ }),
+ ]
+ : []),
+ ]}
>
@@ -209,9 +233,10 @@ export const AnalysisFieldsTable: FC<{
) {
selection = [dependentVariable];
}
- // If nothing selected show minimum fields required message and don't update form yet
+ // If includes is empty show minimum fields required message and don't update form yet
if (selection.length === 0) {
setMinimumFieldsRequiredMessage(minimumFieldsMessage);
+ setUnsupportedFieldsError(undefined);
} else {
setMinimumFieldsRequiredMessage(undefined);
setFormState({ includes: selection });
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index 9dae54b6537b3..571c7731822c0 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -73,6 +73,9 @@ export const ConfigurationStepForm: FC = ({
const [includesTableItems, setIncludesTableItems] = useState([]);
const [maxDistinctValuesError, setMaxDistinctValuesError] = useState();
const [unsupportedFieldsError, setUnsupportedFieldsError] = useState();
+ const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState<
+ undefined | string
+ >();
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
@@ -117,6 +120,7 @@ export const ConfigurationStepForm: FC = ({
dependentVariableEmpty ||
jobType === undefined ||
maxDistinctValuesError !== undefined ||
+ minimumFieldsRequiredMessage !== undefined ||
requiredFieldsError !== undefined ||
unsupportedFieldsError !== undefined;
@@ -400,32 +404,22 @@ export const ConfigurationStepForm: FC = ({
)}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
index a50254334526c..c87f0f4206feb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
@@ -15,13 +15,15 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../../contexts/kibana';
-import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common';
+import {
+ getDataFrameAnalyticsProgressPhase,
+ DATA_FRAME_TASK_STATE,
+} from '../../../analytics_management/components/analytics_list/common';
import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
import { ml } from '../../../../../services/ml_api_service';
import { DataFrameAnalyticsId } from '../../../../common/analytics';
export const PROGRESS_REFRESH_INTERVAL_MS = 1000;
-const FAILED = 'failed';
export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) => {
const [initialized, setInitialized] = useState(false);
@@ -54,7 +56,7 @@ export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) =>
if (jobStats !== undefined) {
const progressStats = getDataFrameAnalyticsProgressPhase(jobStats);
- if (jobStats.state === FAILED) {
+ if (jobStats.state === DATA_FRAME_TASK_STATE.FAILED) {
clearInterval(interval);
setFailedJobMessage(
jobStats.failure_reason ||
@@ -70,8 +72,9 @@ export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) =>
setCurrentProgress(progressStats);
if (
- progressStats.currentPhase === progressStats.totalPhases &&
- progressStats.progress === 100
+ (progressStats.currentPhase === progressStats.totalPhases &&
+ progressStats.progress === 100) ||
+ jobStats.state === DATA_FRAME_TASK_STATE.STOPPED
) {
clearInterval(interval);
}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js
index ef8a7dfcb7417..d989064c5057f 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js
@@ -7,7 +7,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { EuiIcon, keyCodes } from '@elastic/eui';
+import { EuiIcon, keys } from '@elastic/eui';
import { JobGroup } from '../../../job_group';
@@ -63,17 +63,17 @@ export class GroupList extends Component {
};
handleKeyDown = (event, group, index) => {
- switch (event.keyCode) {
- case keyCodes.ENTER:
+ switch (event.key) {
+ case keys.ENTER:
this.selectGroup(group);
break;
- case keyCodes.SPACE:
+ case keys.SPACE:
this.selectGroup(group);
break;
- case keyCodes.DOWN:
+ case keys.ARROW_DOWN:
this.moveDown(event, index);
break;
- case keyCodes.UP:
+ case keys.ARROW_UP:
this.moveUp(event, index);
break;
}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js
index 6a97d32f8cf0c..8118fc7f6df4b 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js
@@ -13,7 +13,7 @@ import {
EuiFlexItem,
EuiFieldText,
EuiFormRow,
- keyCodes,
+ keys,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -61,7 +61,7 @@ export class NewGroupInput extends Component {
newGroupKeyPress = (e) => {
if (
- e.keyCode === keyCodes.ENTER &&
+ e.key === keys.ENTER &&
this.state.groupsValidationError === '' &&
this.state.tempNewGroupName !== ''
) {
diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json
index c3000218aa125..65dd4b373a71a 100644
--- a/x-pack/plugins/monitoring/kibana.json
+++ b/x-pack/plugins/monitoring/kibana.json
@@ -6,5 +6,6 @@
"requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"],
"optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement"]
}
diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json
index 712a46f76bb74..2a04a35830a47 100644
--- a/x-pack/plugins/observability/kibana.json
+++ b/x-pack/plugins/observability/kibana.json
@@ -10,5 +10,10 @@
"licensing"
],
"ui": true,
- "server": true
+ "server": true,
+ "requiredBundles": [
+ "data",
+ "kibanaReact",
+ "kibanaUtils"
+ ]
}
diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
index 61456bc88bd3e..e30eda9f3e056 100644
--- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts
+++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
@@ -77,7 +77,7 @@ export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): I
icon: 'watchesApp',
description: i18n.translate('xpack.observability.emptySection.apps.alert.description', {
defaultMessage:
- '503 errors stacking up. Applications not responding. CPU and RAM utilization jumping. See these warnings as they happen - not as part of the post-mortem.',
+ 'Are 503 errors stacking up? Are services responding? Is CPU and RAM utilization jumping? See warnings as they happen—not as part of the post-mortem.',
}),
linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', {
defaultMessage: 'Create alert',
diff --git a/x-pack/plugins/painless_lab/kibana.json b/x-pack/plugins/painless_lab/kibana.json
index 4b4ea24202846..ca97e73704e70 100644
--- a/x-pack/plugins/painless_lab/kibana.json
+++ b/x-pack/plugins/painless_lab/kibana.json
@@ -12,5 +12,8 @@
"painless_lab"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss
index e6c9574161fd8..c45b0068ded21 100644
--- a/x-pack/plugins/painless_lab/public/styles/_index.scss
+++ b/x-pack/plugins/painless_lab/public/styles/_index.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json
index f1b9d20f762d3..d90d6ea460573 100644
--- a/x-pack/plugins/remote_clusters/kibana.json
+++ b/x-pack/plugins/remote_clusters/kibana.json
@@ -15,5 +15,9 @@
"cloud"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared"
+ ]
}
diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json
index bc1a808d500e0..a5d7f3d20c44c 100644
--- a/x-pack/plugins/reporting/kibana.json
+++ b/x-pack/plugins/reporting/kibana.json
@@ -17,5 +17,9 @@
"share"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaReact",
+ "discover"
+ ]
}
diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap
index b05e74c516cd4..ddba7842f1199 100644
--- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap
+++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap
@@ -129,11 +129,14 @@ Array [
}
/>
@@ -442,12 +443,13 @@ Array [
handler={[Function]}
/>
}
/>
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts
index 928f3b8377809..4f4b41fe0545f 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts
+++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts
@@ -55,10 +55,6 @@ export const args = ({ userDataDir, viewport, disableSandbox, proxy: proxyConfig
flags.push('--no-sandbox');
}
- // log to chrome_debug.log
- flags.push('--enable-logging');
- flags.push('--v=1');
-
if (process.platform === 'linux') {
flags.push('--disable-setuid-sandbox');
}
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
index 3ce5329e42517..157d109e9e27e 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
+++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
@@ -24,7 +24,6 @@ import { CaptureConfig } from '../../../../server/types';
import { LevelLogger } from '../../../lib';
import { safeChildProcess } from '../../safe_child_process';
import { HeadlessChromiumDriver } from '../driver';
-import { getChromeLogLocation } from '../paths';
import { puppeteerLaunch } from '../puppeteer';
import { args } from './args';
@@ -77,7 +76,6 @@ export class HeadlessChromiumDriverFactory {
`The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports.`
);
logger.error(error);
- logger.warning(`See Chromium's log output at "${getChromeLogLocation(this.binaryPath)}"`);
return null;
});
}
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/paths.ts b/x-pack/plugins/reporting/server/browsers/chromium/paths.ts
index 1e760c081f989..c22db895b451e 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/paths.ts
+++ b/x-pack/plugins/reporting/server/browsers/chromium/paths.ts
@@ -7,11 +7,12 @@
import path from 'path';
export const paths = {
- archivesPath: path.resolve(__dirname, '../../../.chromium'),
+ archivesPath: path.resolve(__dirname, '../../../../../../.chromium'),
baseUrl: 'https://storage.googleapis.com/headless_shell/',
packages: [
{
platforms: ['darwin', 'freebsd', 'openbsd'],
+ architecture: 'x64',
archiveFilename: 'chromium-312d84c-darwin.zip',
archiveChecksum: '020303e829745fd332ae9b39442ce570',
binaryChecksum: '5cdec11d45a0eddf782bed9b9f10319f',
@@ -19,13 +20,23 @@ export const paths = {
},
{
platforms: ['linux'],
+ architecture: 'x64',
archiveFilename: 'chromium-312d84c-linux.zip',
archiveChecksum: '15ba9166a42f93ee92e42217b737018d',
binaryChecksum: 'c7fe36ed3e86a6dd23323be0a4e8c0fd',
binaryRelativePath: 'headless_shell-linux/headless_shell',
},
+ {
+ platforms: ['linux'],
+ architecture: 'arm64',
+ archiveFilename: 'chromium-312d84c-linux_arm64.zip',
+ archiveChecksum: 'aa4d5b99dd2c1bd8e614e67f63a48652',
+ binaryChecksum: '7fdccff319396f0aee7f269dd85fe6fc',
+ binaryRelativePath: 'headless_shell-linux_arm64/headless_shell',
+ },
{
platforms: ['win32'],
+ architecture: 'x64',
archiveFilename: 'chromium-312d84c-windows.zip',
archiveChecksum: '3e36adfb755dacacc226ed5fd6b43105',
binaryChecksum: '9913e431fbfc7dfcd958db74ace4d58b',
@@ -33,6 +44,3 @@ export const paths = {
},
],
};
-
-export const getChromeLogLocation = (binaryPath: string) =>
- path.join(binaryPath, '..', 'chrome_debug.log');
diff --git a/x-pack/plugins/reporting/server/browsers/download/clean.ts b/x-pack/plugins/reporting/server/browsers/download/clean.ts
index 8558b001e8174..1a362be8568cd 100644
--- a/x-pack/plugins/reporting/server/browsers/download/clean.ts
+++ b/x-pack/plugins/reporting/server/browsers/download/clean.ts
@@ -29,7 +29,7 @@ export async function clean(dir: string, expectedPaths: string[], logger: LevelL
await asyncMap(filenames, async (filename) => {
const path = resolvePath(dir, filename);
if (!expectedPaths.includes(path)) {
- logger.warn(`Deleting unexpected file ${path}`);
+ logger.warning(`Deleting unexpected file ${path}`);
await del(path, { force: true });
}
});
diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts
index add14448e2f1d..f56af15f5d76b 100644
--- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts
+++ b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts
@@ -7,7 +7,6 @@
import { existsSync } from 'fs';
import { resolve as resolvePath } from 'path';
import { BrowserDownload, chromium } from '../';
-import { BROWSER_TYPE } from '../../../common/constants';
import { LevelLogger } from '../../lib';
import { md5 } from './checksum';
import { clean } from './clean';
@@ -17,19 +16,9 @@ import { asyncMap } from './util';
/**
* Check for the downloaded archive of each requested browser type and
* download them if they are missing or their checksum is invalid
- * @param {String} browserType
* @return {Promise}
*/
-export async function ensureBrowserDownloaded(browserType = BROWSER_TYPE, logger: LevelLogger) {
- await ensureDownloaded([chromium], logger);
-}
-
-/**
- * Check for the downloaded archive of each requested browser type and
- * download them if they are missing or their checksum is invalid*
- * @return {Promise}
- */
-export async function ensureAllBrowsersDownloaded(logger: LevelLogger) {
+export async function ensureBrowserDownloaded(logger: LevelLogger) {
await ensureDownloaded([chromium], logger);
}
diff --git a/x-pack/plugins/reporting/server/browsers/download/index.ts b/x-pack/plugins/reporting/server/browsers/download/index.ts
index bf7ed450b462f..83acec4e0e0b5 100644
--- a/x-pack/plugins/reporting/server/browsers/download/index.ts
+++ b/x-pack/plugins/reporting/server/browsers/download/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { ensureBrowserDownloaded, ensureAllBrowsersDownloaded } from './ensure_downloaded';
+export { ensureBrowserDownloaded } from './ensure_downloaded';
diff --git a/x-pack/plugins/reporting/server/browsers/index.ts b/x-pack/plugins/reporting/server/browsers/index.ts
index be5b869ba523b..0cfe36f6a7656 100644
--- a/x-pack/plugins/reporting/server/browsers/index.ts
+++ b/x-pack/plugins/reporting/server/browsers/index.ts
@@ -12,7 +12,6 @@ import { HeadlessChromiumDriverFactory } from './chromium/driver_factory';
import { installBrowser } from './install';
import { ReportingConfig } from '..';
-export { ensureAllBrowsersDownloaded } from './download';
export { HeadlessChromiumDriver } from './chromium/driver';
export { HeadlessChromiumDriverFactory } from './chromium/driver_factory';
export { chromium } from './chromium';
@@ -42,7 +41,7 @@ export const initializeBrowserDriverFactory = async (
config: ReportingConfig,
logger: LevelLogger
) => {
- const { binaryPath$ } = installBrowser(chromium, config, logger);
+ const { binaryPath$ } = installBrowser(logger);
const binaryPath = await binaryPath$.pipe(first()).toPromise();
const captureConfig = config.get('capture');
return chromium.createDriverFactory(binaryPath, captureConfig, logger);
diff --git a/x-pack/plugins/reporting/server/browsers/install.ts b/x-pack/plugins/reporting/server/browsers/install.ts
index 49361b7b6014d..9eddbe5ef0498 100644
--- a/x-pack/plugins/reporting/server/browsers/install.ts
+++ b/x-pack/plugins/reporting/server/browsers/install.ts
@@ -4,24 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import fs from 'fs';
+import os from 'os';
import path from 'path';
+import del from 'del';
+
import * as Rx from 'rxjs';
-import { first } from 'rxjs/operators';
-import { promisify } from 'util';
-import { ReportingConfig } from '../';
import { LevelLogger } from '../lib';
-import { BrowserDownload } from './';
import { ensureBrowserDownloaded } from './download';
// @ts-ignore
import { md5 } from './download/checksum';
// @ts-ignore
import { extract } from './extract';
-
-const chmod = promisify(fs.chmod);
+import { paths } from './chromium/paths';
interface Package {
platforms: string[];
+ architecture: string;
}
/**
@@ -29,44 +27,33 @@ interface Package {
* archive. If there is an error extracting the archive an `ExtractError` is thrown
*/
export function installBrowser(
- browser: BrowserDownload,
- config: ReportingConfig,
- logger: LevelLogger
+ logger: LevelLogger,
+ chromiumPath: string = path.resolve(__dirname, '../../chromium'),
+ platform: string = process.platform,
+ architecture: string = os.arch()
): { binaryPath$: Rx.Subject } {
const binaryPath$ = new Rx.Subject();
const backgroundInstall = async () => {
- const captureConfig = config.get('capture');
- const { autoDownload, type: browserType } = captureConfig.browser;
- if (autoDownload) {
- await ensureBrowserDownloaded(browserType, logger);
- }
+ const pkg = paths.packages.find((p: Package) => {
+ return p.platforms.includes(platform) && p.architecture === architecture;
+ });
- const pkg = browser.paths.packages.find((p: Package) => p.platforms.includes(process.platform));
if (!pkg) {
- throw new Error(`Unsupported platform: ${JSON.stringify(browser, null, 2)}`);
+ // TODO: validate this
+ throw new Error(`Unsupported platform: ${platform}-${architecture}`);
}
- const dataDir = await config.kbnConfig.get('path', 'data').pipe(first()).toPromise();
- const binaryPath = path.join(dataDir, pkg.binaryRelativePath);
+ const binaryPath = path.join(chromiumPath, pkg.binaryRelativePath);
+ const binaryChecksum = await md5(binaryPath).catch(() => '');
- try {
- const binaryChecksum = await md5(binaryPath).catch(() => '');
+ if (binaryChecksum !== pkg.binaryChecksum) {
+ await ensureBrowserDownloaded(logger);
- if (binaryChecksum !== pkg.binaryChecksum) {
- const archive = path.join(browser.paths.archivesPath, pkg.archiveFilename);
- logger.info(`Extracting [${archive}] to [${binaryPath}]`);
- await extract(archive, dataDir);
- await chmod(binaryPath, '755');
- }
- } catch (error) {
- if (error.cause && ['EACCES', 'EEXIST'].includes(error.cause.code)) {
- logger.error(
- `Error code ${error.cause.code}: Insufficient permissions for extracting the browser archive. ` +
- `Make sure the Kibana data directory (path.data) is owned by the same user that is running Kibana.`
- );
- }
+ const archive = path.join(paths.archivesPath, pkg.archiveFilename);
+ logger.info(`Extracting [${archive}] to [${binaryPath}]`);
- throw error; // reject the promise with the original error
+ await del(chromiumPath);
+ await extract(archive, chromiumPath);
}
logger.debug(`Browser executable: ${binaryPath}`);
diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json
index f897051d3ed8a..e6915f65599cc 100644
--- a/x-pack/plugins/rollup/kibana.json
+++ b/x-pack/plugins/rollup/kibana.json
@@ -15,5 +15,12 @@
"usageCollection",
"visTypeTimeseries"
],
- "configPath": ["xpack", "rollup"]
+ "configPath": ["xpack", "rollup"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "home",
+ "esUiShared",
+ "data"
+ ]
}
diff --git a/x-pack/plugins/searchprofiler/kibana.json b/x-pack/plugins/searchprofiler/kibana.json
index f92083ee9d9fe..a5e42f20b5c7a 100644
--- a/x-pack/plugins/searchprofiler/kibana.json
+++ b/x-pack/plugins/searchprofiler/kibana.json
@@ -5,5 +5,6 @@
"requiredPlugins": ["devTools", "home", "licensing"],
"configPath": ["xpack", "searchprofiler"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["esUiShared"]
}
diff --git a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx
index 27f040f3e9eec..3141f5bedc8f9 100644
--- a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx
@@ -10,7 +10,9 @@ import { EuiScreenReaderOnly } from '@elastic/eui';
import { Editor as AceEditor } from 'brace';
import { initializeEditor } from './init_editor';
-import { useUIAceKeyboardMode } from '../../../../../../src/plugins/es_ui_shared/public';
+import { ace } from '../../../../../../src/plugins/es_ui_shared/public';
+
+const { useUIAceKeyboardMode } = ace;
type EditorShim = ReturnType;
diff --git a/x-pack/plugins/searchprofiler/public/styles/_index.scss b/x-pack/plugins/searchprofiler/public/styles/_index.scss
index e63042cf8fe2f..a33fcc9da53d5 100644
--- a/x-pack/plugins/searchprofiler/public/styles/_index.scss
+++ b/x-pack/plugins/searchprofiler/public/styles/_index.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import 'mixins';
diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json
index 7d1940e393bec..0daab9d5dbce3 100644
--- a/x-pack/plugins/security/kibana.json
+++ b/x-pack/plugins/security/kibana.json
@@ -6,5 +6,13 @@
"requiredPlugins": ["data", "features", "licensing"],
"optionalPlugins": ["home", "management"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "home",
+ "management",
+ "kibanaReact",
+ "spaces",
+ "esUiShared",
+ "management"
+ ]
}
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap
index 050af4bd20a47..48c5680bac4e4 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap
@@ -173,7 +173,9 @@ exports[`APIKeysGridPage renders permission denied if user does not have require
-
+
{
wrapper.update();
});
+ wrapper.update();
const { title, confirmButtonText } = wrapper.find(EuiConfirmModal).props();
expect(title).toMatchInlineSnapshot(`"Delete role mapping 'delete-me'?"`);
expect(confirmButtonText).toMatchInlineSnapshot(`"Delete role mapping"`);
@@ -127,6 +128,7 @@ describe('DeleteProvider', () => {
wrapper.update();
});
+ wrapper.update();
const { title, confirmButtonText } = wrapper.find(EuiConfirmModal).props();
expect(title).toMatchInlineSnapshot(`"Delete 2 role mappings?"`);
expect(confirmButtonText).toMatchInlineSnapshot(`"Delete role mappings"`);
@@ -204,6 +206,7 @@ describe('DeleteProvider', () => {
});
await act(async () => {
+ wrapper.update();
findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click');
await nextTick();
wrapper.update();
@@ -268,6 +271,7 @@ describe('DeleteProvider', () => {
});
await act(async () => {
+ wrapper.update();
findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click');
await nextTick();
wrapper.update();
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
index c457401196ba1..6c43f2f7ea734 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
@@ -88,7 +88,7 @@ export class PrivilegeSpaceForm extends Component {
public render() {
return (
-
+
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
index a4d689121bcaa..16b9de4bae083 100644
--- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
@@ -68,7 +68,9 @@ exports[` renders permission denied if required 1`] = `
-
+
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 90d254b15e8b3..9e7a6f46bbcec 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable @typescript-eslint/no-empty-interface */
+/* eslint-disable @typescript-eslint/camelcase, @typescript-eslint/no-empty-interface */
import * as runtimeTypes from 'io-ts';
import { SavedObjectsClient } from 'kibana/server';
-import { unionWithNullType } from '../../utility_types';
+import { stringEnum, unionWithNullType } from '../../utility_types';
import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note';
import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event';
@@ -164,6 +164,24 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf<
typeof TimelineStatusLiteralWithNullRt
>;
+export enum RowRendererId {
+ auditd = 'auditd',
+ auditd_file = 'auditd_file',
+ netflow = 'netflow',
+ plain = 'plain',
+ suricata = 'suricata',
+ system = 'system',
+ system_dns = 'system_dns',
+ system_endgame_process = 'system_endgame_process',
+ system_file = 'system_file',
+ system_fim = 'system_fim',
+ system_security_event = 'system_security_event',
+ system_socket = 'system_socket',
+ zeek = 'zeek',
+}
+
+export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId');
+
/**
* Timeline template type
*/
@@ -211,6 +229,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
description: unionWithNullType(runtimeTypes.string),
eventType: unionWithNullType(runtimeTypes.string),
+ excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)),
favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)),
filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)),
kqlMode: unionWithNullType(runtimeTypes.string),
diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts
index a12dd926a9181..43271dc40ba12 100644
--- a/x-pack/plugins/security_solution/common/utility_types.ts
+++ b/x-pack/plugins/security_solution/common/utility_types.ts
@@ -15,3 +15,14 @@ export interface DescriptionList {
export const unionWithNullType = (type: T) =>
runtimeTypes.union([type, runtimeTypes.null]);
+
+export const stringEnum = (enumObj: T, enumName = 'enum') =>
+ new runtimeTypes.Type(
+ enumName,
+ (u): u is T[keyof T] => Object.values(enumObj).includes(u),
+ (u, c) =>
+ Object.values(enumObj).includes(u)
+ ? runtimeTypes.success(u as T[keyof T])
+ : runtimeTypes.failure(u, c),
+ (a) => (a as unknown) as string
+ );
diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
index a946fefe273e1..4b1ca19bd96fe 100644
--- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
@@ -7,7 +7,7 @@
export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]';
export const EVENTS_VIEWER_FIELDS_BUTTON =
- '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser-gear"]';
+ '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]';
export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]';
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index f6f2d5171312c..40d3402378895 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -29,5 +29,10 @@
"lists"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaUtils",
+ "esUiShared",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx
index 7807b4a8a77d8..4371a180bc81d 100644
--- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx
@@ -90,7 +90,7 @@ describe('CasesTableFilters ', () => {
wrapper
.find(`[data-test-subj="search-cases"]`)
.last()
- .simulate('keyup', { keyCode: 13, target: { value: 'My search' } });
+ .simulate('keyup', { key: 'Enter', target: { value: 'My search' } });
expect(onFilterChanged).toBeCalledWith({ search: 'My search' });
});
it('should call onFilterChange when status toggled', () => {
diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx
index 5e19211b47078..8d14b2357f450 100644
--- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx
@@ -95,7 +95,11 @@ describe('Configuration button', () => {
);
newWrapper.find('[data-test-subj="configure-case-button"]').first().simulate('mouseOver');
-
- expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`);
+ // EuiToolTip mounts children after a 250ms delay
+ setTimeout(
+ () =>
+ expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`),
+ 250
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx
index 91a5aa5c88beb..7974116f4dc43 100644
--- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx
@@ -166,6 +166,9 @@ describe('ConfigureCases', () => {
expect.objectContaining({
id: '.jira',
}),
+ expect.objectContaining({
+ id: '.resilient',
+ }),
]);
expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false);
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts
index cf5b565b99f67..ba4ecf9a33eee 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { RowRendererId } from '../../../../common/types/timeline';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
@@ -69,5 +70,5 @@ export const alertsHeaders: ColumnHeaderOptions[] = [
export const alertsDefaultModel: SubsetTimelineModel = {
...timelineDefaults,
columns: alertsHeaders,
- showRowRenderers: false,
+ excludedRowRendererIds: Object.values(RowRendererId),
};
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
index e7594365e8103..64f6699d21dac 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
@@ -15,13 +15,13 @@ import {
} from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import deepEqual from 'fast-deep-equal';
import { dragAndDropActions } from '../../store/drag_and_drop';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants';
+
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
-
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
@@ -49,13 +49,27 @@ class DragDropErrorBoundary extends React.PureComponent {
}
}
-const Wrapper = styled.div`
+interface WrapperProps {
+ disabled: boolean;
+}
+
+const Wrapper = styled.div`
display: inline-block;
max-width: 100%;
[data-rbd-placeholder-context-id] {
display: none !important;
}
+
+ ${({ disabled }) =>
+ disabled &&
+ `
+ [data-rbd-draggable-id]:hover,
+ .euiBadge:hover,
+ .euiBadge__text:hover {
+ cursor: default;
+ }
+ `}
`;
Wrapper.displayName = 'Wrapper';
@@ -74,6 +88,7 @@ type RenderFunctionProp = (
interface Props {
dataProvider: DataProvider;
+ disabled?: boolean;
inline?: boolean;
render: RenderFunctionProp;
timelineId?: string;
@@ -100,162 +115,169 @@ export const getStyle = (
};
};
-export const DraggableWrapper = React.memo(
- ({ dataProvider, onFilterAdded, render, timelineId, truncate }) => {
- const draggableRef = useRef(null);
- const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
- const [showTopN, setShowTopN] = useState(false);
- const [goGetTimelineId, setGoGetTimelineId] = useState(false);
- const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId);
- const [providerRegistered, setProviderRegistered] = useState(false);
-
- const dispatch = useDispatch();
-
- const handleClosePopOverTrigger = useCallback(
- () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger),
- []
- );
-
- const toggleTopN = useCallback(() => {
- setShowTopN((prevShowTopN) => {
- const newShowTopN = !prevShowTopN;
- if (newShowTopN === false) {
- handleClosePopOverTrigger();
- }
- return newShowTopN;
- });
- }, [handleClosePopOverTrigger]);
-
- const registerProvider = useCallback(() => {
- if (!providerRegistered) {
- dispatch(dragAndDropActions.registerProvider({ provider: dataProvider }));
- setProviderRegistered(true);
+const DraggableWrapperComponent: React.FC = ({
+ dataProvider,
+ onFilterAdded,
+ render,
+ timelineId,
+ truncate,
+}) => {
+ const draggableRef = useRef(null);
+ const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
+ const [showTopN, setShowTopN] = useState(false);
+ const [goGetTimelineId, setGoGetTimelineId] = useState(false);
+ const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId);
+ const [providerRegistered, setProviderRegistered] = useState(false);
+ const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
+ const dispatch = useDispatch();
+
+ const handleClosePopOverTrigger = useCallback(
+ () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger),
+ []
+ );
+
+ const toggleTopN = useCallback(() => {
+ setShowTopN((prevShowTopN) => {
+ const newShowTopN = !prevShowTopN;
+ if (newShowTopN === false) {
+ handleClosePopOverTrigger();
}
- }, [dispatch, providerRegistered, dataProvider]);
-
- const unRegisterProvider = useCallback(
- () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })),
- [dispatch, dataProvider]
- );
-
- useEffect(
- () => () => {
- unRegisterProvider();
- },
- [unRegisterProvider]
- );
-
- const hoverContent = useMemo(
- () => (
-
- ),
- [
- dataProvider,
- handleClosePopOverTrigger,
- onFilterAdded,
- showTopN,
- timelineId,
- timelineIdFind,
- toggleTopN,
- ]
- );
-
- const renderContent = useCallback(
- () => (
-
-
- (
-
-
-
- {render(dataProvider, provided, snapshot)}
-
-
-
- )}
- >
- {(droppableProvided) => (
-
-
{
+ if (!isDisabled) {
+ dispatch(dragAndDropActions.registerProvider({ provider: dataProvider }));
+ setProviderRegistered(true);
+ }
+ }, [isDisabled, dispatch, dataProvider]);
+
+ const unRegisterProvider = useCallback(
+ () =>
+ providerRegistered &&
+ dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })),
+ [providerRegistered, dispatch, dataProvider.id]
+ );
+
+ useEffect(
+ () => () => {
+ unRegisterProvider();
+ },
+ [unRegisterProvider]
+ );
+
+ const hoverContent = useMemo(
+ () => (
+
+ ),
+ [
+ dataProvider,
+ handleClosePopOverTrigger,
+ onFilterAdded,
+ showTopN,
+ timelineId,
+ timelineIdFind,
+ toggleTopN,
+ ]
+ );
+
+ const renderContent = useCallback(
+ () => (
+
+
+ (
+
+
+
- {(provided, snapshot) => (
- {
- provided.innerRef(e);
- draggableRef.current = e;
- }}
- data-test-subj="providerContainer"
- isDragging={snapshot.isDragging}
- registerProvider={registerProvider}
- >
- {truncate && !snapshot.isDragging ? (
-
- {render(dataProvider, provided, snapshot)}
-
- ) : (
-
- {render(dataProvider, provided, snapshot)}
-
- )}
-
- )}
-
- {droppableProvided.placeholder}
+ {render(dataProvider, provided, snapshot)}
+
- )}
-
-
-
- ),
- [dataProvider, render, registerProvider, truncate]
- );
-
- return (
-
- );
- },
- (prevProps, nextProps) =>
- deepEqual(prevProps.dataProvider, nextProps.dataProvider) &&
- prevProps.render !== nextProps.render &&
- prevProps.truncate === nextProps.truncate
-);
+
+ )}
+ >
+ {(droppableProvided) => (
+
+
+ {(provided, snapshot) => (
+ {
+ provided.innerRef(e);
+ draggableRef.current = e;
+ }}
+ data-test-subj="providerContainer"
+ isDragging={snapshot.isDragging}
+ registerProvider={registerProvider}
+ >
+ {truncate && !snapshot.isDragging ? (
+
+ {render(dataProvider, provided, snapshot)}
+
+ ) : (
+
+ {render(dataProvider, provided, snapshot)}
+
+ )}
+
+ )}
+
+ {droppableProvided.placeholder}
+
+ )}
+
+
+
+ ),
+ [dataProvider, registerProvider, render, isDisabled, truncate]
+ );
+
+ if (isDisabled) return <>{renderContent()}>;
+
+ return (
+
+ );
+};
+
+export const DraggableWrapper = React.memo(DraggableWrapperComponent);
DraggableWrapper.displayName = 'DraggableWrapper';
diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx
index 62a07550650aa..4dc3c6fcbe440 100644
--- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx
@@ -5,13 +5,16 @@
*/
import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui';
-import React from 'react';
+import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../drag_and_drop/helpers';
import { getEmptyStringTag } from '../empty_value';
-import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
+import {
+ DataProvider,
+ IS_OPERATOR,
+} from '../../../timelines/components/timeline/data_providers/data_provider';
import { Provider } from '../../../timelines/components/timeline/data_providers/provider';
export interface DefaultDraggableType {
@@ -84,36 +87,48 @@ Content.displayName = 'Content';
* @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data
*/
export const DefaultDraggable = React.memo(
- ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) =>
- value != null ? (
+ ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => {
+ const dataProviderProp: DataProvider = useMemo(
+ () => ({
+ and: [],
+ enabled: true,
+ id: escapeDataProviderId(id),
+ name: name ? name : value ?? '',
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field,
+ value: queryValue ? queryValue : value ?? '',
+ operator: IS_OPERATOR,
+ },
+ }),
+ [field, id, name, queryValue, value]
+ );
+
+ const renderCallback = useCallback(
+ (dataProvider, _, snapshot) =>
+ snapshot.isDragging ? (
+
+
+
+ ) : (
+
+ {children}
+
+ ),
+ [children, field, tooltipContent, value]
+ );
+
+ if (value == null) return null;
+
+ return (
- snapshot.isDragging ? (
-
-
-
- ) : (
-
- {children}
-
- )
- }
+ dataProvider={dataProviderProp}
+ render={renderCallback}
timelineId={timelineId}
/>
- ) : null
+ );
+ }
);
DefaultDraggable.displayName = 'DefaultDraggable';
@@ -146,33 +161,34 @@ export type BadgeDraggableType = Omit & {
* prevent a tooltip from being displayed, or pass arbitrary content
* @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data
*/
-export const DraggableBadge = React.memo(
- ({
- contextId,
- eventId,
- field,
- value,
- iconType,
- name,
- color = 'hollow',
- children,
- tooltipContent,
- queryValue,
- }) =>
- value != null ? (
-
-
- {children ? children : value !== '' ? value : getEmptyStringTag()}
-
-
- ) : null
-);
+const DraggableBadgeComponent: React.FC = ({
+ contextId,
+ eventId,
+ field,
+ value,
+ iconType,
+ name,
+ color = 'hollow',
+ children,
+ tooltipContent,
+ queryValue,
+}) =>
+ value != null ? (
+
+
+ {children ? children : value !== '' ? value : getEmptyStringTag()}
+
+
+ ) : null;
+
+DraggableBadgeComponent.displayName = 'DraggableBadgeComponent';
+export const DraggableBadge = React.memo(DraggableBadgeComponent);
DraggableBadge.displayName = 'DraggableBadge';
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index 2a079ce015f0d..38ca1176d1700 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -77,7 +77,7 @@ describe('EventsViewer', () => {
await wait();
wrapper.update();
- expect(wrapper.find(`[data-test-subj="show-field-browser-gear"]`).first().exists()).toBe(true);
+ expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
test('it renders the footer containing the Load More button', async () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index 02b3571421f67..b89d2b8c08625 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -45,6 +45,7 @@ const StatefulEventsViewerComponent: React.FC = ({
defaultIndices,
deleteEventQuery,
end,
+ excludedRowRendererIds,
filters,
headerFilterGroup,
id,
@@ -57,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({
removeColumn,
start,
showCheckboxes,
- showRowRenderers,
sort,
updateItemsPerPage,
upsertColumn,
@@ -69,7 +69,14 @@ const StatefulEventsViewerComponent: React.FC = ({
useEffect(() => {
if (createTimeline != null) {
- createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers });
+ createTimeline({
+ id,
+ columns,
+ excludedRowRendererIds,
+ sort,
+ itemsPerPage,
+ showCheckboxes,
+ });
}
return () => {
deleteEventQuery({ id, inputId: 'global' });
@@ -125,7 +132,7 @@ const StatefulEventsViewerComponent: React.FC = ({
onChangeItemsPerPage={onChangeItemsPerPage}
query={query}
start={start}
- sort={sort!}
+ sort={sort}
toggleColumn={toggleColumn}
utilityBar={utilityBar}
/>
@@ -145,18 +152,19 @@ const makeMapStateToProps = () => {
columns,
dataProviders,
deletedEventIds,
+ excludedRowRendererIds,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
sort,
showCheckboxes,
- showRowRenderers,
} = events;
return {
columns,
dataProviders,
deletedEventIds,
+ excludedRowRendererIds,
filters: getGlobalFiltersQuerySelector(state),
id,
isLive: input.policy.kind === 'interval',
@@ -166,7 +174,6 @@ const makeMapStateToProps = () => {
query: getGlobalQuerySelector(state),
sort,
showCheckboxes,
- showRowRenderers,
};
};
return mapStateToProps;
@@ -192,6 +199,7 @@ export const StatefulEventsViewer = connector(
deepEqual(prevProps.columns, nextProps.columns) &&
deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
+ deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
prevProps.end === nextProps.end &&
deepEqual(prevProps.filters, nextProps.filters) &&
@@ -204,7 +212,6 @@ export const StatefulEventsViewer = connector(
prevProps.start === nextProps.start &&
deepEqual(prevProps.pageFilters, nextProps.pageFilters) &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
- prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.start === nextProps.start &&
prevProps.utilityBar === nextProps.utilityBar
)
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index be89aa8e33718..10d510c5f56c3 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -43,6 +43,7 @@ import {
defaultEndpointExceptionItems,
entryHasListType,
entryHasNonEcsType,
+ getMappedNonEcsValue,
} from '../helpers';
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
@@ -65,7 +66,7 @@ interface AddExceptionModalProps {
nonEcsData: TimelineNonEcsData[];
};
onCancel: () => void;
- onConfirm: () => void;
+ onConfirm: (didCloseAlert: boolean) => void;
}
const Modal = styled(EuiModal)`
@@ -130,8 +131,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({
);
const onSuccess = useCallback(() => {
displaySuccessToast(i18n.ADD_EXCEPTION_SUCCESS, dispatchToaster);
- onConfirm();
- }, [dispatchToaster, onConfirm]);
+ onConfirm(shouldCloseAlert);
+ }, [dispatchToaster, onConfirm, shouldCloseAlert]);
const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException(
{
@@ -193,6 +194,12 @@ export const AddExceptionModal = memo(function AddExceptionModal({
indexPatterns,
]);
+ useEffect(() => {
+ if (shouldDisableBulkClose === true) {
+ setShouldBulkCloseAlert(false);
+ }
+ }, [shouldDisableBulkClose]);
+
const onCommentChange = useCallback(
(value: string) => {
setComment(value);
@@ -214,6 +221,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({
[setShouldBulkCloseAlert]
);
+ const retrieveAlertOsTypes = useCallback(() => {
+ const osDefaults = ['windows', 'macos', 'linux'];
+ if (alertData) {
+ const osTypes = getMappedNonEcsValue({
+ data: alertData.nonEcsData,
+ fieldName: 'host.os.family',
+ });
+ if (osTypes.length === 0) {
+ return osDefaults;
+ }
+ return osTypes;
+ }
+ return osDefaults;
+ }, [alertData]);
+
const enrichExceptionItems = useCallback(() => {
let enriched: Array = [];
enriched =
@@ -221,11 +243,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({
? enrichExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }])
: exceptionItemsToAdd;
if (exceptionListType === 'endpoint') {
- const osTypes = alertData ? ['windows'] : ['windows', 'macos', 'linux'];
+ const osTypes = retrieveAlertOsTypes();
enriched = enrichExceptionItemsWithOS(enriched, osTypes);
}
return enriched;
- }, [comment, exceptionItemsToAdd, exceptionListType, alertData]);
+ }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]);
const onAddExceptionConfirm = useCallback(() => {
if (addOrUpdateExceptionItems !== null) {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index aa36b65e04b69..0ef3350fbefc7 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -37,7 +37,7 @@ import { AddExceptionComments } from '../add_exception_comments';
import {
enrichExceptionItemsWithComments,
enrichExceptionItemsWithOS,
- getOsTagValues,
+ getOperatingSystems,
entryHasListType,
entryHasNonEcsType,
} from '../helpers';
@@ -135,6 +135,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
indexPatterns,
]);
+ useEffect(() => {
+ if (shouldDisableBulkClose === true) {
+ setShouldBulkCloseAlert(false);
+ }
+ }, [shouldDisableBulkClose]);
+
const handleBuilderOnChange = useCallback(
({
exceptionItems,
@@ -167,7 +173,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
...(comment !== '' ? [{ comment }] : []),
]);
if (exceptionListType === 'endpoint') {
- const osTypes = exceptionItem._tags ? getOsTagValues(exceptionItem._tags) : ['windows'];
+ const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : [];
enriched = enrichExceptionItemsWithOS(enriched, osTypes);
}
return enriched;
@@ -199,6 +205,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({
{!isSignalIndexLoading && (
<>
+ {i18n.EXCEPTION_BUILDER_INFO}
+
{
beforeEach(() => {
@@ -248,6 +258,36 @@ describe('Exception helpers', () => {
});
});
+ describe('#getEntryValue', () => {
+ it('returns "match" entry value', () => {
+ const payload = getEntryMatchMock();
+ const result = getEntryValue(payload);
+ const expected = 'some host name';
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "match any" entry values', () => {
+ const payload = getEntryMatchAnyMock();
+ const result = getEntryValue(payload);
+ const expected = ['some host name'];
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "exists" entry value', () => {
+ const payload = getEntryExistsMock();
+ const result = getEntryValue(payload);
+ const expected = undefined;
+ expect(result).toEqual(expected);
+ });
+
+ it('returns "list" entry value', () => {
+ const payload = getEntryListMock();
+ const result = getEntryValue(payload);
+ const expected = 'some-list-id';
+ expect(result).toEqual(expected);
+ });
+ });
+
describe('#formatEntry', () => {
test('it formats an entry', () => {
const payload = getEntryMatchMock();
@@ -280,25 +320,55 @@ describe('Exception helpers', () => {
test('it returns null if no operating system tag specified', () => {
const result = getOperatingSystems(['some tag', 'some other tag']);
- expect(result).toEqual('');
+ expect(result).toEqual([]);
});
test('it returns null if operating system tag malformed', () => {
const result = getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag']);
+ expect(result).toEqual([]);
+ });
+
+ test('it returns operating systems if space included in os tag', () => {
+ const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag']);
+ expect(result).toEqual(['macos']);
+ });
+
+ test('it returns operating systems if multiple os tags specified', () => {
+ const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows']);
+ expect(result).toEqual(['macos', 'windows']);
+ });
+ });
+
+ describe('#formatOperatingSystems', () => {
+ test('it returns null if no operating system tag specified', () => {
+ const result = formatOperatingSystems(getOperatingSystems(['some tag', 'some other tag']));
+
+ expect(result).toEqual('');
+ });
+
+ test('it returns null if operating system tag malformed', () => {
+ const result = formatOperatingSystems(
+ getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag'])
+ );
+
expect(result).toEqual('');
});
test('it returns formatted operating systems if space included in os tag', () => {
- const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag']);
+ const result = formatOperatingSystems(
+ getOperatingSystems(['some tag', 'os: macos', 'some other tag'])
+ );
- expect(result).toEqual('Mac');
+ expect(result).toEqual('macOS');
});
test('it returns formatted operating systems if multiple os tags specified', () => {
- const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag', 'os:windows']);
+ const result = formatOperatingSystems(
+ getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows'])
+ );
- expect(result).toEqual('Mac, Windows');
+ expect(result).toEqual('macOS, Windows');
});
});
@@ -441,4 +511,176 @@ describe('Exception helpers', () => {
expect(exceptions).toEqual([{ ...rest, meta: undefined }]);
});
});
+
+ describe('#formatExceptionItemForUpdate', () => {
+ test('it should return correct update fields', () => {
+ const payload = getExceptionListItemSchemaMock();
+ const result = formatExceptionItemForUpdate(payload);
+ const expected = {
+ _tags: ['endpoint', 'process', 'malware', 'os:linux'],
+ comments: [],
+ description: 'This is a sample endpoint type exception',
+ entries: ENTRIES,
+ id: '1',
+ item_id: 'endpoint_list_item',
+ meta: {},
+ name: 'Sample Endpoint Exception List',
+ namespace_type: 'single',
+ tags: ['user added string for a tag', 'malware'],
+ type: 'simple',
+ };
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('#enrichExceptionItemsWithComments', () => {
+ test('it should add comments to an exception item', () => {
+ const payload = [getExceptionListItemSchemaMock()];
+ const comments = getCommentsArrayMock();
+ const result = enrichExceptionItemsWithComments(payload, comments);
+ const expected = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ comments: getCommentsArrayMock(),
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+
+ test('it should add comments to multiple exception items', () => {
+ const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
+ const comments = getCommentsArrayMock();
+ const result = enrichExceptionItemsWithComments(payload, comments);
+ const expected = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ comments: getCommentsArrayMock(),
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ comments: getCommentsArrayMock(),
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('#enrichExceptionItemsWithOS', () => {
+ test('it should add an os tag to an exception item', () => {
+ const payload = [getExceptionListItemSchemaMock()];
+ const osTypes = ['windows'];
+ const result = enrichExceptionItemsWithOS(payload, osTypes);
+ const expected = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ _tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows'],
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+
+ test('it should add multiple os tags to all exception items', () => {
+ const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
+ const osTypes = ['windows', 'macos'];
+ const result = enrichExceptionItemsWithOS(payload, osTypes);
+ const expected = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ _tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ _tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+
+ test('it should add os tag to all exception items without duplication', () => {
+ const payload = [
+ { ...getExceptionListItemSchemaMock(), _tags: ['os:linux', 'os:windows'] },
+ { ...getExceptionListItemSchemaMock(), _tags: ['os:linux'] },
+ ];
+ const osTypes = ['windows'];
+ const result = enrichExceptionItemsWithOS(payload, osTypes);
+ const expected = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ _tags: ['os:linux', 'os:windows'],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ _tags: ['os:linux', 'os:windows'],
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('#entryHasListType', () => {
+ test('it should return false with an empty array', () => {
+ const payload: ExceptionListItemSchema[] = [];
+ const result = entryHasListType(payload);
+ expect(result).toEqual(false);
+ });
+
+ test("it should return false with exception items that don't contain a list type", () => {
+ const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
+ const result = entryHasListType(payload);
+ expect(result).toEqual(false);
+ });
+
+ test('it should return true with exception items that do contain a list type', () => {
+ const payload = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [{ type: OperatorTypeEnum.LIST }] as EntriesArray,
+ },
+ getExceptionListItemSchemaMock(),
+ ];
+ const result = entryHasListType(payload);
+ expect(result).toEqual(true);
+ });
+ });
+
+ describe('#entryHasNonEcsType', () => {
+ const mockEcsIndexPattern = {
+ title: 'testIndex',
+ fields: [
+ {
+ name: 'some.parentField',
+ },
+ {
+ name: 'some.not.nested.field',
+ },
+ {
+ name: 'nested.field',
+ },
+ ],
+ } as IIndexPattern;
+
+ test('it should return false with an empty array', () => {
+ const payload: ExceptionListItemSchema[] = [];
+ const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
+ expect(result).toEqual(false);
+ });
+
+ test("it should return false with exception items that don't contain a non ecs type", () => {
+ const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
+ const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
+ expect(result).toEqual(false);
+ });
+
+ test('it should return true with exception items that do contain a non ecs type', () => {
+ const payload = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [{ field: 'some.nonEcsField' }] as EntriesArray,
+ },
+ getExceptionListItemSchemaMock(),
+ ];
+ const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
+ expect(result).toEqual(true);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
index db7cb5aeac8f0..481b2736b7597 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -14,7 +14,6 @@ import * as i18n from './translations';
import {
FormattedEntry,
BuilderEntry,
- EmptyListEntry,
DescriptionListItem,
FormattedBuilderEntry,
CreateExceptionListItemBuilderSchema,
@@ -39,9 +38,6 @@ import {
ExceptionListType,
} from '../../../lists_plugin_deps';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
-
-export const isListType = (item: BuilderEntry): item is EmptyListEntry =>
- item.type === OperatorTypeEnum.LIST;
import { TimelineNonEcsData } from '../../../graphql/types';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
@@ -82,11 +78,6 @@ export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption =
}
};
-export const getExceptionOperatorFromSelect = (value: string): OperatorOption => {
- const operator = EXCEPTION_OPERATORS.filter(({ message }) => message === value);
- return operator[0] ?? isOperator;
-};
-
/**
* Formats ExceptionItem entries into simple field, operator, value
* for use in rendering items in table
@@ -158,19 +149,32 @@ export const formatEntry = ({
};
};
-export const getOperatingSystems = (tags: string[]): string => {
- const osMatches = tags
- .filter((tag) => tag.startsWith('os:'))
- .map((os) => capitalize(os.substring(3).trim()))
- .join(', ');
-
- return osMatches;
+/**
+ * Retrieves the values of tags marked as os
+ *
+ * @param tags an ExceptionItem's tags
+ */
+export const getOperatingSystems = (tags: string[]): string[] => {
+ return tags.filter((tag) => tag.startsWith('os:')).map((os) => os.substring(3).trim());
};
-export const getOsTagValues = (tags: string[]): string[] => {
- return tags.filter((tag) => tag.startsWith('os:')).map((os) => os.substring(3).trim());
+/**
+ * Formats os value array to a displayable string
+ */
+export const formatOperatingSystems = (osTypes: string[]): string => {
+ return osTypes
+ .map((os) => {
+ if (os === 'macos') {
+ return 'macOS';
+ }
+ return capitalize(os);
+ })
+ .join(', ');
};
+/**
+ * Returns all tags that match a given regex
+ */
export const getTagsInclude = ({
tags,
regex,
@@ -194,7 +198,7 @@ export const getDescriptionListContent = (
const details = [
{
title: i18n.OPERATING_SYSTEM,
- value: getOperatingSystems(exceptionItem._tags),
+ value: formatOperatingSystems(getOperatingSystems(exceptionItem._tags ?? [])),
},
{
title: i18n.DATE_CREATED,
@@ -376,6 +380,11 @@ export const formatExceptionItemForUpdate = (
};
};
+/**
+ * Adds new and existing comments to all new exceptionItems if not present already
+ * @param exceptionItems new or existing ExceptionItem[]
+ * @param comments new Comments
+ */
export const enrichExceptionItemsWithComments = (
exceptionItems: Array,
comments: Array
@@ -388,6 +397,11 @@ export const enrichExceptionItemsWithComments = (
});
};
+/**
+ * Adds provided osTypes to all exceptionItems if not present already
+ * @param exceptionItems new or existing ExceptionItem[]
+ * @param osTypes array of os values
+ */
export const enrichExceptionItemsWithOS = (
exceptionItems: Array,
osTypes: string[]
@@ -402,18 +416,21 @@ export const enrichExceptionItemsWithOS = (
});
};
+/**
+ * Returns the value for the given fieldname within TimelineNonEcsData if it exists
+ */
export const getMappedNonEcsValue = ({
data,
fieldName,
}: {
data: TimelineNonEcsData[];
fieldName: string;
-}): string[] | undefined => {
+}): string[] => {
const item = data.find((d) => d.field === fieldName);
if (item != null && item.value != null) {
return item.value;
}
- return undefined;
+ return [];
};
export const entryHasListType = (
@@ -421,7 +438,7 @@ export const entryHasListType = (
) => {
for (const { entries } of exceptionItems) {
for (const exceptionEntry of entries ?? []) {
- if (getOperatorType(exceptionEntry) === 'list') {
+ if (getOperatorType(exceptionEntry) === OperatorTypeEnum.LIST) {
return true;
}
}
@@ -429,16 +446,29 @@ export const entryHasListType = (
return false;
};
+/**
+ * Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping
+ */
export const entryHasNonEcsType = (
exceptionItems: Array,
indexPatterns: IIndexPattern
): boolean => {
+ const doesFieldNameExist = (exceptionEntry: Entry): boolean => {
+ return indexPatterns.fields.some(({ name }) => name === exceptionEntry.field);
+ };
+
if (exceptionItems.length === 0) {
return false;
}
for (const { entries } of exceptionItems) {
for (const exceptionEntry of entries ?? []) {
- if (indexPatterns.fields.find(({ name }) => name === exceptionEntry.field) === undefined) {
+ if (exceptionEntry.type === 'nested') {
+ for (const nestedExceptionEntry of exceptionEntry.entries) {
+ if (doesFieldNameExist(nestedExceptionEntry) === false) {
+ return true;
+ }
+ }
+ } else if (doesFieldNameExist(exceptionEntry) === false) {
return true;
}
}
@@ -446,19 +476,25 @@ export const entryHasNonEcsType = (
return false;
};
+/**
+ * Returns the default values from the alert data to autofill new endpoint exceptions
+ */
export const defaultEndpointExceptionItems = (
listType: ExceptionListType,
listId: string,
ruleName: string,
alertData: TimelineNonEcsData[]
): ExceptionsBuilderExceptionItem[] => {
- const [filePath] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.path' }) ?? [];
- const [signatureSigner] =
- getMappedNonEcsValue({ data: alertData, fieldName: 'file.Ext.code_signature.subject_name' }) ??
- [];
- const [signatureTrusted] =
- getMappedNonEcsValue({ data: alertData, fieldName: 'file.Ext.code_signature.trusted' }) ?? [];
- const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }) ?? [];
+ const [filePath] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.path' });
+ const [signatureSigner] = getMappedNonEcsValue({
+ data: alertData,
+ fieldName: 'file.Ext.code_signature.subject_name',
+ });
+ const [signatureTrusted] = getMappedNonEcsValue({
+ data: alertData,
+ fieldName: 'file.Ext.code_signature.trusted',
+ });
+ const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' });
const namespaceType = 'agnostic';
return [
@@ -483,7 +519,7 @@ export const defaultEndpointExceptionItems = (
value: signatureSigner ?? '',
},
{
- field: 'file.code_signature.trusted',
+ field: 'file.Ext.code_signature.trusted',
operator: 'included',
type: 'match',
value: signatureTrusted ?? '',
@@ -508,7 +544,7 @@ export const defaultEndpointExceptionItems = (
field: 'event.category',
operator: 'included',
type: 'match_any',
- value: getMappedNonEcsValue({ data: alertData, fieldName: 'event.category' }) ?? [],
+ value: getMappedNonEcsValue({ data: alertData, fieldName: 'event.category' }),
},
],
},
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
index cdfa358a0f9c2..3d9fe2ebaddae 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
@@ -184,7 +184,11 @@ const ExceptionsViewerComponent = ({
[setCurrentModal]
);
- const handleCloseExceptionModal = useCallback((): void => {
+ const handleOnCancelExceptionModal = useCallback((): void => {
+ setCurrentModal(null);
+ }, [setCurrentModal]);
+
+ const handleOnConfirmExceptionModal = useCallback((): void => {
setCurrentModal(null);
handleFetchList();
}, [setCurrentModal, handleFetchList]);
@@ -255,8 +259,8 @@ const ExceptionsViewerComponent = ({
ruleName={ruleName}
exceptionListType={exceptionListTypeToEdit}
exceptionItem={exceptionToEdit}
- onCancel={handleCloseExceptionModal}
- onConfirm={handleCloseExceptionModal}
+ onCancel={handleOnCancelExceptionModal}
+ onConfirm={handleOnConfirmExceptionModal}
/>
)}
@@ -265,8 +269,8 @@ const ExceptionsViewerComponent = ({
ruleName={ruleName}
ruleId={ruleId}
exceptionListType={exceptionListTypeToEdit}
- onCancel={handleCloseExceptionModal}
- onConfirm={handleCloseExceptionModal}
+ onCancel={handleOnCancelExceptionModal}
+ onConfirm={handleOnConfirmExceptionModal}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
index 26775608637c0..0f93e954ab853 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
@@ -225,6 +225,12 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"subdued": "#81858f",
"warning": "#ffce7a",
},
+ "euiFacetGutterSizes": Object {
+ "gutterLarge": "12px",
+ "gutterMedium": "8px",
+ "gutterNone": 0,
+ "gutterSmall": "4px",
+ },
"euiFilePickerTallHeight": "128px",
"euiFlyoutBorder": "1px solid #343741",
"euiFocusBackgroundColor": "#232635",
@@ -272,6 +278,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"euiGradientMiddle": "#282a31",
"euiGradientStartStop": "#2e3039",
"euiHeaderBackgroundColor": "#1d1e24",
+ "euiHeaderBorderColor": "#343741",
"euiHeaderBreadcrumbColor": "#d4dae5",
"euiHeaderChildSize": "48px",
"euiHeaderHeight": "48px",
@@ -589,9 +596,9 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"top": "euiToolTipTop",
},
"euiTooltipBackgroundColor": "#000000",
- "euiZComboBox": 8001,
"euiZContent": 0,
"euiZContentMenu": 2000,
+ "euiZFlyout": 3000,
"euiZHeader": 1000,
"euiZLevel0": 0,
"euiZLevel1": 1000,
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
index 0b19e4177f5c2..833f85712b5fa 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
@@ -7,9 +7,11 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common';
import { connector as jiraConnectorConfig } from './jira/config';
+import { connector as resilientConnectorConfig } from './resilient/config';
import { ConnectorConfiguration } from './types';
export const connectorsConfiguration: Record = {
'.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration,
'.jira': jiraConnectorConfig,
+ '.resilient': resilientConnectorConfig,
};
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
index 83b07a2905ef0..f32e1e0df184e 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
@@ -5,3 +5,4 @@
*/
export { getActionType as jiraActionType } from './jira';
+export { getActionType as resilientActionType } from './resilient';
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts
new file mode 100644
index 0000000000000..7d4edbf624877
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ConnectorConfiguration } from './types';
+
+import * as i18n from './translations';
+import logo from './logo.svg';
+
+export const connector: ConnectorConfiguration = {
+ id: '.resilient',
+ name: i18n.RESILIENT_TITLE,
+ logo,
+ enabled: true,
+ enabledInConfig: true,
+ enabledInLicense: true,
+ minimumLicenseRequired: 'platinum',
+ fields: {
+ name: {
+ label: i18n.MAPPING_FIELD_NAME,
+ validSourceFields: ['title', 'description'],
+ defaultSourceField: 'title',
+ defaultActionType: 'overwrite',
+ },
+ description: {
+ label: i18n.MAPPING_FIELD_DESC,
+ validSourceFields: ['title', 'description'],
+ defaultSourceField: 'description',
+ defaultActionType: 'overwrite',
+ },
+ comments: {
+ label: i18n.MAPPING_FIELD_COMMENTS,
+ validSourceFields: ['comments'],
+ defaultSourceField: 'comments',
+ defaultActionType: 'append',
+ },
+ },
+};
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx
new file mode 100644
index 0000000000000..31bf0a4dfc34b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx
@@ -0,0 +1,114 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import {
+ EuiFieldText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiFieldPassword,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import * as i18n from './translations';
+import { ConnectorFlyoutFormProps } from '../types';
+import { ResilientActionConnector } from './types';
+import { withConnectorFlyout } from '../components/connector_flyout';
+
+const resilientConnectorForm: React.FC> = ({
+ errors,
+ action,
+ onChangeSecret,
+ onBlurSecret,
+ onChangeConfig,
+ onBlurConfig,
+}) => {
+ const { orgId } = action.config;
+ const { apiKeyId, apiKeySecret } = action.secrets;
+ const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null;
+ const isApiKeyIdInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null;
+ const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null;
+
+ return (
+ <>
+
+
+
+ onChangeConfig('orgId', evt.target.value)}
+ onBlur={() => onBlurConfig('orgId')}
+ />
+
+
+
+
+
+
+
+ onChangeSecret('apiKeyId', evt.target.value)}
+ onBlur={() => onBlurSecret('apiKeyId')}
+ />
+
+
+
+
+
+
+
+ onChangeSecret('apiKeySecret', evt.target.value)}
+ onBlur={() => onBlurSecret('apiKeySecret')}
+ />
+
+
+
+ >
+ );
+};
+
+export const resilientConnectorFlyout = withConnectorFlyout({
+ ConnectorFormComponent: resilientConnectorForm,
+ secretKeys: ['apiKeyId', 'apiKeySecret'],
+ configKeys: ['orgId'],
+ connectorActionTypeId: '.resilient',
+});
+
+// eslint-disable-next-line import/no-default-export
+export { resilientConnectorFlyout as default };
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx
new file mode 100644
index 0000000000000..d3daf195582a8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.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 { lazy } from 'react';
+import {
+ ValidationResult,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../../../../triggers_actions_ui/public/types';
+
+import { connector } from './config';
+import { createActionType } from '../utils';
+import logo from './logo.svg';
+import { ResilientActionConnector } from './types';
+import * as i18n from './translations';
+
+interface Errors {
+ orgId: string[];
+ apiKeyId: string[];
+ apiKeySecret: string[];
+}
+
+const validateConnector = (action: ResilientActionConnector): ValidationResult => {
+ const errors: Errors = {
+ orgId: [],
+ apiKeyId: [],
+ apiKeySecret: [],
+ };
+
+ if (!action.config.orgId) {
+ errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_LABEL];
+ }
+
+ if (!action.secrets.apiKeyId) {
+ errors.apiKeyId = [...errors.apiKeyId, i18n.RESILIENT_API_KEY_ID_REQUIRED];
+ }
+
+ if (!action.secrets.apiKeySecret) {
+ errors.apiKeySecret = [...errors.apiKeySecret, i18n.RESILIENT_API_KEY_SECRET_REQUIRED];
+ }
+
+ return { errors };
+};
+
+export const getActionType = createActionType({
+ id: connector.id,
+ iconClass: logo,
+ selectMessage: i18n.RESILIENT_DESC,
+ actionTypeTitle: connector.name,
+ validateConnector,
+ actionConnectorFields: lazy(() => import('./flyout')),
+});
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg
new file mode 100644
index 0000000000000..553c2c62b7191
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts
new file mode 100644
index 0000000000000..f8aec2eea3d4b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export * from '../translations';
+
+export const RESILIENT_DESC = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.selectMessageText',
+ {
+ defaultMessage: 'Push or update SIEM case data to a new issue in resilient',
+ }
+);
+
+export const RESILIENT_TITLE = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.actionTypeTitle',
+ {
+ defaultMessage: 'IBM Resilient',
+ }
+);
+
+export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.orgId',
+ {
+ defaultMessage: 'Organization Id',
+ }
+);
+
+export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField',
+ {
+ defaultMessage: 'Organization Id',
+ }
+);
+
+export const RESILIENT_API_KEY_ID_LABEL = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.apiKeyId',
+ {
+ defaultMessage: 'API key id',
+ }
+);
+
+export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField',
+ {
+ defaultMessage: 'API key id is required',
+ }
+);
+
+export const RESILIENT_API_KEY_SECRET_LABEL = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.apiKeySecret',
+ {
+ defaultMessage: 'API key secret',
+ }
+);
+
+export const RESILIENT_API_KEY_SECRET_REQUIRED = i18n.translate(
+ 'xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField',
+ {
+ defaultMessage: 'API key secret is required',
+ }
+);
+
+export const MAPPING_FIELD_NAME = i18n.translate(
+ 'xpack.securitySolution.case.configureCases.mappingFieldName',
+ {
+ defaultMessage: 'Name',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts
new file mode 100644
index 0000000000000..fe6dbb2b3674a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable no-restricted-imports */
+/* eslint-disable @kbn/eslint/no-restricted-paths */
+
+import {
+ ResilientPublicConfigurationType,
+ ResilientSecretConfigurationType,
+} from '../../../../../../actions/server/builtin_action_types/resilient/types';
+
+export { ResilientFieldsType } from '../../../../../../case/common/api/connectors';
+
+export * from '../types';
+
+export interface ResilientActionConnector {
+ config: ResilientPublicConfigurationType;
+ secrets: ResilientSecretConfigurationType;
+}
diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
index 3d76416855e9e..89f100992e1b9 100644
--- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
@@ -195,6 +195,7 @@ export const mockGlobalState: State = {
dataProviders: [],
description: '',
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -215,7 +216,6 @@ export const mockGlobalState: State = {
},
selectedEventIds: {},
show: false,
- showRowRenderers: true,
showCheckboxes: false,
pinnedEventIds: {},
pinnedEventsSaveObject: {},
diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts
index 7503062300d2d..9974842bff474 100644
--- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts
@@ -418,8 +418,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] },
{ field: 'host.name', value: ['zeek-franfurt'] },
- { field: 'source.ip', value: ['185.176.26.101'] },
- { field: 'destination.ip', value: ['207.154.238.205'] },
+ { field: 'source.ip', value: ['192.168.26.101'] },
+ { field: 'destination.ip', value: ['192.168.238.205'] },
],
ecs: {
_id: '14',
@@ -466,8 +466,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] },
{ field: 'host.name', value: ['suricata-zeek-singapore'] },
- { field: 'source.ip', value: ['206.189.35.240'] },
- { field: 'destination.ip', value: ['67.207.67.3'] },
+ { field: 'source.ip', value: ['192.168.35.240'] },
+ { field: 'destination.ip', value: ['192.168.67.3'] },
],
ecs: {
_id: '15',
@@ -520,8 +520,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] },
{ field: 'host.name', value: ['suricata-zeek-singapore'] },
- { field: 'source.ip', value: ['206.189.35.240'] },
- { field: 'destination.ip', value: ['192.241.164.26'] },
+ { field: 'source.ip', value: ['192.168.35.240'] },
+ { field: 'destination.ip', value: ['192.168.164.26'] },
],
ecs: {
_id: '16',
@@ -572,7 +572,7 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] },
{ field: 'host.name', value: ['zeek-franfurt'] },
- { field: 'source.ip', value: ['8.42.77.171'] },
+ { field: 'source.ip', value: ['192.168.77.171'] },
],
ecs: {
_id: '17',
@@ -621,8 +621,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] },
{ field: 'host.name', value: ['zeek-sensor-amsterdam'] },
- { field: 'source.ip', value: ['188.166.66.184'] },
- { field: 'destination.ip', value: ['91.189.95.15'] },
+ { field: 'source.ip', value: ['192.168.66.184'] },
+ { field: 'destination.ip', value: ['192.168.95.15'] },
],
ecs: {
_id: '18',
@@ -767,7 +767,7 @@ export const mockTimelineData: TimelineItem[] = [
{ field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] },
{ field: 'event.category', value: ['user-login'] },
{ field: 'host.name', value: ['zeek-london'] },
- { field: 'source.ip', value: ['8.42.77.171'] },
+ { field: 'source.ip', value: ['192.168.77.171'] },
{ field: 'user.name', value: ['root'] },
],
ecs: {
@@ -1101,7 +1101,7 @@ export const mockTimelineData: TimelineItem[] = [
{ field: 'event.action', value: ['connected-to'] },
{ field: 'event.category', value: ['audit-rule'] },
{ field: 'host.name', value: ['zeek-london'] },
- { field: 'destination.ip', value: ['93.184.216.34'] },
+ { field: 'destination.ip', value: ['192.168.216.34'] },
{ field: 'user.name', value: ['alice'] },
],
ecs: {
@@ -1121,7 +1121,7 @@ export const mockTimelineData: TimelineItem[] = [
data: null,
summary: {
actor: { primary: ['alice'], secondary: ['alice'] },
- object: { primary: ['93.184.216.34'], secondary: ['80'], type: ['socket'] },
+ object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] },
how: ['/usr/bin/wget'],
message_type: null,
sequence: null,
@@ -1133,7 +1133,7 @@ export const mockTimelineData: TimelineItem[] = [
ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'],
},
source: null,
- destination: { ip: ['93.184.216.34'], port: [80] },
+ destination: { ip: ['192.168.216.34'], port: [80] },
geo: null,
suricata: null,
network: null,
@@ -1174,7 +1174,7 @@ export const mockTimelineData: TimelineItem[] = [
},
auditd: {
result: ['success'],
- session: ['unset'],
+ session: ['242'],
data: null,
summary: {
actor: { primary: ['unset'], secondary: ['root'] },
diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
index 5248136437d7d..b1df41a19aebe 100644
--- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
@@ -2098,6 +2098,7 @@ export const mockTimelineModel: TimelineModel = {
description: 'This is a sample rule description',
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [
{
$state: {
@@ -2137,7 +2138,6 @@ export const mockTimelineModel: TimelineModel = {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: Direction.desc,
@@ -2217,6 +2217,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
description: '',
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@@ -2241,7 +2242,6 @@ export const defaultTimelineProps: CreateTimelineProps = {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
status: TimelineStatus.draft,
title: '',
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index 2fa7cfeedcd15..1213312e2a22c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -158,6 +158,7 @@ describe('alert actions', () => {
description: 'This is a sample rule description',
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [
{
$state: {
@@ -210,7 +211,6 @@ describe('alert actions', () => {
selectedEventIds: {},
show: true,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx
index 0ceb2c87dd5ea..6533be1a9b09c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx
@@ -39,6 +39,10 @@ interface AlertsUtilityBarProps {
updateAlertsStatus: UpdateAlertsStatus;
}
+const UtilityBarFlexGroup = styled(EuiFlexGroup)`
+ min-width: 175px;
+`;
+
const AlertsUtilityBarComponent: React.FC = ({
canUserCRUD,
hasIndexWrite,
@@ -69,10 +73,6 @@ const AlertsUtilityBarComponent: React.FC = ({
defaultNumberFormat
);
- const UtilityBarFlexGroup = styled(EuiFlexGroup)`
- min-width: 175px;
- `;
-
const UtilityBarPopoverContent = (closePopover: () => void) => (
{currentFilter !== FILTER_OPEN && (
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
index 697dff4012982..319575c9c307f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
@@ -11,6 +11,7 @@ import ApolloClient from 'apollo-client';
import { Dispatch } from 'redux';
import { EuiText } from '@elastic/eui';
+import { RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
import {
@@ -36,7 +37,7 @@ import {
SetEventsLoadingProps,
UpdateTimelineLoading,
} from './types';
-import { Ecs } from '../../../graphql/types';
+import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
import { AddExceptionOnClick } from '../../../common/components/exceptions/add_exception_modal';
import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers';
@@ -162,7 +163,7 @@ export const alertsDefaultModel: SubsetTimelineModel = {
...timelineDefaults,
columns: alertsHeaders,
showCheckboxes: true,
- showRowRenderers: false,
+ excludedRowRendererIds: Object.values(RowRendererId),
};
export const requiredFieldsForActions = [
@@ -174,6 +175,8 @@ export const requiredFieldsForActions = [
'signal.rule.query',
'signal.rule.to',
'signal.rule.id',
+ 'signal.original_event.kind',
+ 'signal.original_event.module',
// Endpoint exception fields
'file.path',
@@ -189,6 +192,7 @@ interface AlertActionArgs {
createTimeline: CreateTimeline;
dispatch: Dispatch;
ecsRowData: Ecs;
+ nonEcsRowData: TimelineNonEcsData[];
hasIndexWrite: boolean;
onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
@@ -211,6 +215,7 @@ export const getAlertActions = ({
createTimeline,
dispatch,
ecsRowData,
+ nonEcsRowData,
hasIndexWrite,
onAlertStatusUpdateFailure,
onAlertStatusUpdateSuccess,
@@ -281,6 +286,18 @@ export const getAlertActions = ({
width: DEFAULT_ICON_BUTTON_WIDTH,
};
+ const isEndpointAlert = () => {
+ const [module] = getMappedNonEcsValue({
+ data: nonEcsRowData,
+ fieldName: 'signal.original_event.module',
+ });
+ const [kind] = getMappedNonEcsValue({
+ data: nonEcsRowData,
+ fieldName: 'signal.original_event.kind',
+ });
+ return module === 'endpoint' && kind === 'alert';
+ };
+
return [
{
...getInvestigateInResolverAction({ dispatch, timelineId }),
@@ -305,15 +322,14 @@ export const getAlertActions = ({
...(FILTER_OPEN !== status ? [openAlertActionComponent] : []),
...(FILTER_CLOSED !== status ? [closeAlertActionComponent] : []),
...(FILTER_IN_PROGRESS !== status ? [inProgressAlertActionComponent] : []),
- // TODO: disable this option if the alert is not an Endpoint alert
{
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
- const ruleNameValue = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
- const ruleId = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
- if (ruleId !== undefined && ruleId.length > 0) {
+ const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
+ const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
+ if (ruleId !== undefined) {
openAddExceptionModal({
- ruleName: ruleNameValue ? ruleNameValue[0] : '',
- ruleId: ruleId[0],
+ ruleName: ruleName ?? '',
+ ruleId,
exceptionListType: 'endpoint',
alertData: {
ecsData,
@@ -323,7 +339,7 @@ export const getAlertActions = ({
}
},
id: 'addEndpointException',
- isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
+ isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !isEndpointAlert(),
dataTestSubj: 'add-endpoint-exception-menu-item',
ariaLabel: 'Add Endpoint Exception',
content: {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} ,
@@ -331,12 +347,12 @@ export const getAlertActions = ({
},
{
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
- const ruleNameValue = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
- const ruleId = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
- if (ruleId !== undefined && ruleId.length > 0) {
+ const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
+ const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
+ if (ruleId !== undefined) {
openAddExceptionModal({
- ruleName: ruleNameValue ? ruleNameValue[0] : '',
- ruleId: ruleId[0],
+ ruleName: ruleName ?? '',
+ ruleId,
exceptionListType: 'detection',
alertData: {
ecsData,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index 81aebe95930ac..b9b963a84e966 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -22,7 +22,10 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import {
+ useManageTimeline,
+ TimelineRowActionArgs,
+} from '../../../timelines/components/manage_timeline';
import { useApolloClient } from '../../../common/utils/apollo_context';
import { updateAlertStatusAction } from './actions';
@@ -48,7 +51,6 @@ import {
displaySuccessToast,
displayErrorToast,
} from '../../../common/components/toasters';
-import { Ecs } from '../../../graphql/types';
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
import {
AddExceptionModal,
@@ -321,12 +323,13 @@ export const AlertsTableComponent: React.FC = ({
// Send to Timeline / Update Alert Status Actions for each table row
const additionalActions = useMemo(
- () => (ecsRowData: Ecs) =>
+ () => ({ ecsData, nonEcsData }: TimelineRowActionArgs) =>
getAlertActions({
apolloClient,
canUserCRUD,
createTimeline: createTimelineCallback,
- ecsRowData,
+ ecsRowData: ecsData,
+ nonEcsRowData: nonEcsData,
dispatch,
hasIndexWrite,
onAlertStatusUpdateFailure,
@@ -401,9 +404,12 @@ export const AlertsTableComponent: React.FC = ({
closeAddExceptionModal();
}, [closeAddExceptionModal]);
- const onAddExceptionConfirm = useCallback(() => {
- closeAddExceptionModal();
- }, [closeAddExceptionModal]);
+ const onAddExceptionConfirm = useCallback(
+ (didCloseAlert: boolean) => {
+ closeAddExceptionModal();
+ },
+ [closeAddExceptionModal]
+ );
if (loading || isEmpty(signalsIndex)) {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts
index d043127a3098b..30d1e30417583 100644
--- a/x-pack/plugins/security_solution/public/detections/index.ts
+++ b/x-pack/plugins/security_solution/public/detections/index.ts
@@ -10,7 +10,7 @@ import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline';
import { AlertsRoutes } from './routes';
import { SecuritySubPlugin } from '../app/types';
-const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [
+const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [
TimelineId.detectionsRulesDetailsPage,
TimelineId.detectionsPage,
];
@@ -22,7 +22,7 @@ export class Detections {
return {
SubPluginRoutes: AlertsRoutes,
storageTimelines: {
- timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS),
+ timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS),
},
};
}
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 2b8b07cb6a24b..20978fa3b063c 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -9641,6 +9641,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "excludedRowRendererIds",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "favorite",
"description": "",
@@ -10146,6 +10162,75 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "RowRendererId",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ { "name": "auditd", "description": "", "isDeprecated": false, "deprecationReason": null },
+ {
+ "name": "auditd_file",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "netflow",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ { "name": "plain", "description": "", "isDeprecated": false, "deprecationReason": null },
+ {
+ "name": "suricata",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ { "name": "system", "description": "", "isDeprecated": false, "deprecationReason": null },
+ {
+ "name": "system_dns",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system_endgame_process",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system_file",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system_fim",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system_security_event",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system_socket",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ { "name": "zeek", "description": "", "isDeprecated": false, "deprecationReason": null }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "FavoriteTimelineResult",
@@ -11061,6 +11146,20 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
+ {
+ "name": "excludedRowRendererIds",
+ "description": "",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null }
+ }
+ },
+ "defaultValue": null
+ },
{
"name": "filters",
"description": "",
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index 2c8f2e63356e6..27aa02038097e 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -124,6 +124,8 @@ export interface TimelineInput {
eventType?: Maybe;
+ excludedRowRendererIds?: Maybe;
+
filters?: Maybe;
kqlMode?: Maybe;
@@ -349,6 +351,22 @@ export enum DataProviderType {
template = 'template',
}
+export enum RowRendererId {
+ auditd = 'auditd',
+ auditd_file = 'auditd_file',
+ netflow = 'netflow',
+ plain = 'plain',
+ suricata = 'suricata',
+ system = 'system',
+ system_dns = 'system_dns',
+ system_endgame_process = 'system_endgame_process',
+ system_file = 'system_file',
+ system_fim = 'system_fim',
+ system_security_event = 'system_security_event',
+ system_socket = 'system_socket',
+ zeek = 'zeek',
+}
+
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@@ -1961,6 +1979,8 @@ export interface TimelineResult {
eventType?: Maybe;
+ excludedRowRendererIds?: Maybe;
+
favorite?: Maybe;
filters?: Maybe;
@@ -4385,6 +4405,8 @@ export namespace GetAllTimeline {
eventIdToNoteIds: Maybe;
+ excludedRowRendererIds: Maybe;
+
notes: Maybe;
noteIds: Maybe;
@@ -5454,6 +5476,8 @@ export namespace GetOneTimeline {
eventIdToNoteIds: Maybe;
+ excludedRowRendererIds: Maybe;
+
favorite: Maybe;
filters: Maybe;
diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx
index 95cc76a349c17..73c5c1e37da0f 100644
--- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx
@@ -35,6 +35,10 @@ Percent.displayName = 'Percent';
const SourceDestinationArrowsContainer = styled(EuiFlexGroup)`
margin: 0 2px;
+
+ .euiToolTipAnchor {
+ white-space: nowrap;
+ }
`;
SourceDestinationArrowsContainer.displayName = 'SourceDestinationArrowsContainer';
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 6096a9b0e0bb8..7bb4be6b50879 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -22,7 +22,7 @@ import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { initTelemetry } from './common/lib/telemetry';
import { KibanaServices } from './common/lib/kibana/services';
-import { jiraActionType } from './common/lib/connectors';
+import { jiraActionType, resilientActionType } from './common/lib/connectors';
import {
PluginSetup,
PluginStart,
@@ -84,6 +84,7 @@ export class Plugin implements IPlugin {
const storage = new Storage(localStorage);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
index b5c4e6481216c..be0ba04c53233 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
@@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { EuiBreadcrumbs, Breadcrumb, EuiCode, EuiBetaBadge } from '@elastic/eui';
+import { EuiBreadcrumbs, EuiBreadcrumb, EuiCode, EuiBetaBadge } from '@elastic/eui';
import styled from 'styled-components';
import React, { memo } from 'react';
import { useResolverTheme } from '../assets';
@@ -58,7 +58,7 @@ export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({
breadcrumbs,
truncate,
}: {
- breadcrumbs: Breadcrumb[];
+ breadcrumbs: EuiBreadcrumb[];
truncate?: boolean;
}) {
const {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx
index 92ef5c41f3b4c..ed3f957ad11a8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx
@@ -6,11 +6,9 @@
import { mount } from 'enzyme';
import React from 'react';
-import { ActionCreator } from 'typescript-fsa';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
-import { ColumnHeaderOptions } from '../../store/timeline/model';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers';
@@ -29,17 +27,6 @@ afterAll(() => {
console.warn = originalWarn;
});
-const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{
- id: string;
- columnId: string;
-}>;
-
-const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{
- column: ColumnHeaderOptions;
- id: string;
- index: number;
-}>;
-
describe('StatefulFieldsBrowser', () => {
const timelineId = 'test';
@@ -54,13 +41,11 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
- expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns');
+ expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true);
});
describe('toggleShow', () => {
@@ -75,8 +60,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
@@ -95,8 +78,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
@@ -122,8 +103,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
@@ -149,8 +128,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
@@ -186,39 +163,14 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
- />
-
- );
-
- expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true);
- });
-
- test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => {
- const isEventViewer = false;
-
- const wrapper = mount(
-
-
);
- expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false);
+ expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true);
});
- test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => {
+ test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => {
const isEventViewer = true;
const wrapper = mount(
@@ -232,12 +184,10 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
- removeColumn={removeColumnMock}
- upsertColumn={upsertColumnMock}
/>
);
- expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false);
+ expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
index a3937107936b6..7b843b4f69447 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
@@ -4,14 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
-import { timelineActions } from '../../store/timeline';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers';
import { FieldsBrowser } from './field_browser';
@@ -34,181 +32,156 @@ FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer';
/**
* Manages the state of the field browser
*/
-export const StatefulFieldsBrowserComponent = React.memo(
- ({
- columnHeaders,
- browserFields,
- height,
- isEventViewer = false,
- onFieldSelected,
- onUpdateColumns,
- timelineId,
- toggleColumn,
- width,
- }) => {
- /** tracks the latest timeout id from `setTimeout`*/
- const inputTimeoutId = useRef(0);
-
- /** all field names shown in the field browser must contain this string (when specified) */
- const [filterInput, setFilterInput] = useState('');
- /** all fields in this collection have field names that match the filterInput */
- const [filteredBrowserFields, setFilteredBrowserFields] = useState(null);
- /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */
- const [isSearching, setIsSearching] = useState(false);
- /** this category will be displayed in the right-hand pane of the field browser */
- const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME);
- /** show the field browser */
- const [show, setShow] = useState(false);
- useEffect(() => {
- return () => {
- if (inputTimeoutId.current !== 0) {
- // ⚠️ mutation: cancel any remaining timers and zero-out the timer id:
- clearTimeout(inputTimeoutId.current);
- inputTimeoutId.current = 0;
- }
- };
- }, []);
-
- /** Shows / hides the field browser */
- const toggleShow = useCallback(() => {
- setShow(!show);
- }, [show]);
-
- /** Invoked when the user types in the filter input */
- const updateFilter = useCallback(
- (newFilterInput: string) => {
- setFilterInput(newFilterInput);
- setIsSearching(true);
- if (inputTimeoutId.current !== 0) {
- clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
- }
- // ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
- inputTimeoutId.current = window.setTimeout(() => {
- const newFilteredBrowserFields = filterBrowserFieldsByFieldName({
- browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
- substring: newFilterInput,
- });
- setFilteredBrowserFields(newFilteredBrowserFields);
- setIsSearching(false);
-
- const newSelectedCategoryId =
- newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0
- ? DEFAULT_CATEGORY_NAME
- : Object.keys(newFilteredBrowserFields)
- .sort()
- .reduce(
- (selected, category) =>
- newFilteredBrowserFields[category].fields != null &&
- newFilteredBrowserFields[selected].fields != null &&
- Object.keys(newFilteredBrowserFields[category].fields!).length >
- Object.keys(newFilteredBrowserFields[selected].fields!).length
- ? category
- : selected,
- Object.keys(newFilteredBrowserFields)[0]
- );
- setSelectedCategoryId(newSelectedCategoryId);
- }, INPUT_TIMEOUT);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [browserFields, filterInput, inputTimeoutId.current]
- );
-
- /**
- * Invoked when the user clicks a category name in the left-hand side of
- * the field browser
- */
- const updateSelectedCategoryId = useCallback((categoryId: string) => {
- setSelectedCategoryId(categoryId);
- }, []);
-
- /**
- * Invoked when the user clicks on the context menu to view a category's
- * columns in the timeline, this function dispatches the action that
- * causes the timeline display those columns.
- */
- const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => {
- onUpdateColumns(columns); // show the category columns in the timeline
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- /** Invoked when the field browser should be hidden */
- const hideFieldBrowser = useCallback(() => {
- setFilterInput('');
- setFilterInput('');
- setFilteredBrowserFields(null);
- setIsSearching(false);
- setSelectedCategoryId(DEFAULT_CATEGORY_NAME);
- setShow(false);
- }, []);
- // only merge in the default category if the field browser is visible
- const browserFieldsWithDefaultCategory = useMemo(
- () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}),
- [show, browserFields]
- );
-
- return (
- <>
-
-
- {isEventViewer ? (
-
- ) : (
-
- {i18n.FIELDS}
-
- )}
-
-
- {show && (
-
- )}
-
- >
- );
- }
-);
-
-const mapDispatchToProps = {
- removeColumn: timelineActions.removeColumn,
- upsertColumn: timelineActions.upsertColumn,
+export const StatefulFieldsBrowserComponent: React.FC = ({
+ columnHeaders,
+ browserFields,
+ height,
+ isEventViewer = false,
+ onFieldSelected,
+ onUpdateColumns,
+ timelineId,
+ toggleColumn,
+ width,
+}) => {
+ /** tracks the latest timeout id from `setTimeout`*/
+ const inputTimeoutId = useRef(0);
+
+ /** all field names shown in the field browser must contain this string (when specified) */
+ const [filterInput, setFilterInput] = useState('');
+ /** all fields in this collection have field names that match the filterInput */
+ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null);
+ /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */
+ const [isSearching, setIsSearching] = useState(false);
+ /** this category will be displayed in the right-hand pane of the field browser */
+ const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME);
+ /** show the field browser */
+ const [show, setShow] = useState(false);
+ useEffect(() => {
+ return () => {
+ if (inputTimeoutId.current !== 0) {
+ // ⚠️ mutation: cancel any remaining timers and zero-out the timer id:
+ clearTimeout(inputTimeoutId.current);
+ inputTimeoutId.current = 0;
+ }
+ };
+ }, []);
+
+ /** Shows / hides the field browser */
+ const toggleShow = useCallback(() => {
+ setShow(!show);
+ }, [show]);
+
+ /** Invoked when the user types in the filter input */
+ const updateFilter = useCallback(
+ (newFilterInput: string) => {
+ setFilterInput(newFilterInput);
+ setIsSearching(true);
+ if (inputTimeoutId.current !== 0) {
+ clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
+ }
+ // ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
+ inputTimeoutId.current = window.setTimeout(() => {
+ const newFilteredBrowserFields = filterBrowserFieldsByFieldName({
+ browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
+ substring: newFilterInput,
+ });
+ setFilteredBrowserFields(newFilteredBrowserFields);
+ setIsSearching(false);
+
+ const newSelectedCategoryId =
+ newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0
+ ? DEFAULT_CATEGORY_NAME
+ : Object.keys(newFilteredBrowserFields)
+ .sort()
+ .reduce(
+ (selected, category) =>
+ newFilteredBrowserFields[category].fields != null &&
+ newFilteredBrowserFields[selected].fields != null &&
+ Object.keys(newFilteredBrowserFields[category].fields!).length >
+ Object.keys(newFilteredBrowserFields[selected].fields!).length
+ ? category
+ : selected,
+ Object.keys(newFilteredBrowserFields)[0]
+ );
+ setSelectedCategoryId(newSelectedCategoryId);
+ }, INPUT_TIMEOUT);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [browserFields, filterInput, inputTimeoutId.current]
+ );
+
+ /**
+ * Invoked when the user clicks a category name in the left-hand side of
+ * the field browser
+ */
+ const updateSelectedCategoryId = useCallback((categoryId: string) => {
+ setSelectedCategoryId(categoryId);
+ }, []);
+
+ /**
+ * Invoked when the user clicks on the context menu to view a category's
+ * columns in the timeline, this function dispatches the action that
+ * causes the timeline display those columns.
+ */
+ const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => {
+ onUpdateColumns(columns); // show the category columns in the timeline
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ /** Invoked when the field browser should be hidden */
+ const hideFieldBrowser = useCallback(() => {
+ setFilterInput('');
+ setFilterInput('');
+ setFilteredBrowserFields(null);
+ setIsSearching(false);
+ setSelectedCategoryId(DEFAULT_CATEGORY_NAME);
+ setShow(false);
+ }, []);
+ // only merge in the default category if the field browser is visible
+ const browserFieldsWithDefaultCategory = useMemo(
+ () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}),
+ [show, browserFields]
+ );
+
+ return (
+
+
+
+ {i18n.FIELDS}
+
+
+
+ {show && (
+
+ )}
+
+ );
};
-const connector = connect(null, mapDispatchToProps);
-
-type PropsFromRedux = ConnectedProps;
-
-export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent));
+export const StatefulFieldsBrowser = React.memo(StatefulFieldsBrowserComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
index fbe3c475c9fe6..8c03d82aafafb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
@@ -28,6 +28,7 @@ interface FlyoutPaneComponentProps {
const EuiFlyoutContainer = styled.div`
.timeline-flyout {
+ z-index: 4001;
min-width: 150px;
width: auto;
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
index 99d79a2441ccc..7882185cbd9d6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
@@ -14,7 +14,7 @@ import { SubsetTimelineModel } from '../../store/timeline/model';
import * as i18n from '../../../common/components/events_viewer/translations';
import * as i18nF from '../timeline/footer/translations';
import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults';
-import { Ecs } from '../../../graphql/types';
+import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
interface ManageTimelineInit {
documentType?: string;
@@ -25,11 +25,16 @@ interface ManageTimelineInit {
indexToAdd?: string[] | null;
loadingText?: string;
selectAll?: boolean;
- timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
+ timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
title?: string;
unit?: (totalCount: number) => string;
}
+export interface TimelineRowActionArgs {
+ ecsData: Ecs;
+ nonEcsData: TimelineNonEcsData[];
+}
+
interface ManageTimeline {
documentType: string;
defaultModel: SubsetTimelineModel;
@@ -41,7 +46,7 @@ interface ManageTimeline {
loadingText: string;
queryFields: string[];
selectAll: boolean;
- timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
+ timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
title: string;
unit: (totalCount: number) => string;
}
@@ -71,7 +76,7 @@ type ActionManageTimeline =
id: string;
payload: {
queryFields?: string[];
- timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
+ timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
};
};
@@ -142,7 +147,7 @@ interface UseTimelineManager {
setTimelineRowActions: (actionsArgs: {
id: string;
queryFields?: string[];
- timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
+ timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
}) => void;
}
@@ -167,7 +172,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT
}: {
id: string;
queryFields?: string[];
- timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
+ timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
}) => {
dispatch({
type: 'SET_TIMELINE_ACTIONS',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
index 22f89ffc6927e..eeb789c14a8f8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
@@ -225,6 +225,12 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"subdued": "#81858f",
"warning": "#ffce7a",
},
+ "euiFacetGutterSizes": Object {
+ "gutterLarge": "12px",
+ "gutterMedium": "8px",
+ "gutterNone": 0,
+ "gutterSmall": "4px",
+ },
"euiFilePickerTallHeight": "128px",
"euiFlyoutBorder": "1px solid #343741",
"euiFocusBackgroundColor": "#232635",
@@ -272,6 +278,7 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"euiGradientMiddle": "#282a31",
"euiGradientStartStop": "#2e3039",
"euiHeaderBackgroundColor": "#1d1e24",
+ "euiHeaderBorderColor": "#343741",
"euiHeaderBreadcrumbColor": "#d4dae5",
"euiHeaderChildSize": "48px",
"euiHeaderHeight": "48px",
@@ -589,9 +596,9 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"top": "euiToolTipTop",
},
"euiTooltipBackgroundColor": "#000000",
- "euiZComboBox": 8001,
"euiZContent": 0,
"euiZContentMenu": 2000,
+ "euiZFlyout": 3000,
"euiZHeader": 1000,
"euiZLevel0": 0,
"euiZLevel1": 1000,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
index 31ac3240afb72..89a35fb838a96 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
@@ -270,6 +270,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@@ -294,7 +295,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@@ -368,6 +368,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@@ -392,7 +393,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@@ -502,6 +502,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@@ -532,7 +533,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@@ -628,6 +628,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [
{
$state: {
@@ -701,7 +702,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
index 48706c4f23906..e2def46b936be 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
@@ -100,7 +100,7 @@ describe('StatefulOpenTimeline', () => {
);
wrapper
.find('[data-test-subj="search-bar"] input')
- .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } });
+ .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } });
expect(wrapper.find('[data-test-subj="search-row"]').first().prop('query')).toEqual('abcd');
});
@@ -122,7 +122,7 @@ describe('StatefulOpenTimeline', () => {
wrapper
.find('[data-test-subj="search-bar"] input')
- .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } });
+ .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } });
expect(wrapper.find('[data-test-subj="query-message"]').first().text()).toContain(
'Showing: 11 timelines with'
@@ -147,7 +147,7 @@ describe('StatefulOpenTimeline', () => {
wrapper
.find('[data-test-subj="search-bar"] input')
- .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } });
+ .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } });
expect(wrapper.find('[data-test-subj="selectable-query-text"]').first().text()).toEqual(
'with "abcd"'
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
index 2e6dcb85ad769..18c2e4cff16bf 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
@@ -138,7 +138,7 @@ describe('SearchRow', () => {
wrapper
.find('[data-test-subj="search-bar"] input')
- .simulate('keyup', { keyCode: 13, target: { value: 'abcd' } });
+ .simulate('keyup', { key: 'Enter', target: { value: 'abcd' } });
expect(onQueryChange).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index c21edaa916588..a8485328e8393 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -13,6 +13,7 @@ import {
TimelineTypeLiteralWithNull,
TimelineStatus,
TemplateTimelineTypeLiteral,
+ RowRendererId,
} from '../../../../common/types/timeline';
/** The users who added a timeline to favorites */
@@ -46,6 +47,7 @@ export interface OpenTimelineResult {
created?: number | null;
description?: string | null;
eventIdToNoteIds?: Readonly> | null;
+ excludedRowRendererIds?: RowRendererId[] | null;
favorite?: FavoriteTimelineResult[] | null;
noteIds?: string[] | null;
notes?: TimelineResultNote[] | null;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx
new file mode 100644
index 0000000000000..55d1694297e2e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx
@@ -0,0 +1,199 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiLink } from '@elastic/eui';
+import React from 'react';
+import { ExternalLinkIcon } from '../../../../common/components/external_link_icon';
+
+import { RowRendererId } from '../../../../../common/types/timeline';
+import {
+ AuditdExample,
+ AuditdFileExample,
+ NetflowExample,
+ SuricataExample,
+ SystemExample,
+ SystemDnsExample,
+ SystemEndgameProcessExample,
+ SystemFileExample,
+ SystemFimExample,
+ SystemSecurityEventExample,
+ SystemSocketExample,
+ ZeekExample,
+} from '../examples';
+import * as i18n from './translations';
+
+const Link = ({ children, url }: { children: React.ReactNode; url: string }) => (
+
+ {children}
+
+
+);
+
+export interface RowRendererOption {
+ id: RowRendererId;
+ name: string;
+ description: React.ReactNode;
+ searchableDescription: string;
+ example: React.ReactNode;
+}
+
+export const renderers: RowRendererOption[] = [
+ {
+ id: RowRendererId.auditd,
+ name: i18n.AUDITD_NAME,
+ description: (
+
+
+ {i18n.AUDITD_NAME}
+ {' '}
+ {i18n.AUDITD_DESCRIPTION_PART1}
+
+ ),
+ example: AuditdExample,
+ searchableDescription: `${i18n.AUDITD_NAME} ${i18n.AUDITD_DESCRIPTION_PART1}`,
+ },
+ {
+ id: RowRendererId.auditd_file,
+ name: i18n.AUDITD_FILE_NAME,
+ description: (
+
+
+ {i18n.AUDITD_NAME}
+ {' '}
+ {i18n.AUDITD_FILE_DESCRIPTION_PART1}
+
+ ),
+ example: AuditdFileExample,
+ searchableDescription: `${i18n.AUDITD_FILE_NAME} ${i18n.AUDITD_FILE_DESCRIPTION_PART1}`,
+ },
+ {
+ id: RowRendererId.system_security_event,
+ name: i18n.AUTHENTICATION_NAME,
+ description: (
+
+
{i18n.AUTHENTICATION_DESCRIPTION_PART1}
+
+
{i18n.AUTHENTICATION_DESCRIPTION_PART2}
+
+ ),
+ example: SystemSecurityEventExample,
+ searchableDescription: `${i18n.AUTHENTICATION_DESCRIPTION_PART1} ${i18n.AUTHENTICATION_DESCRIPTION_PART2}`,
+ },
+ {
+ id: RowRendererId.system_dns,
+ name: i18n.DNS_NAME,
+ description: i18n.DNS_DESCRIPTION_PART1,
+ example: SystemDnsExample,
+ searchableDescription: i18n.DNS_DESCRIPTION_PART1,
+ },
+ {
+ id: RowRendererId.netflow,
+ name: i18n.FLOW_NAME,
+ description: (
+
+
{i18n.FLOW_DESCRIPTION_PART1}
+
+
{i18n.FLOW_DESCRIPTION_PART2}
+
+ ),
+ example: NetflowExample,
+ searchableDescription: `${i18n.FLOW_DESCRIPTION_PART1} ${i18n.FLOW_DESCRIPTION_PART2}`,
+ },
+ {
+ id: RowRendererId.system,
+ name: i18n.SYSTEM_NAME,
+ description: (
+
+
+ {i18n.SYSTEM_DESCRIPTION_PART1}{' '}
+
+ {i18n.SYSTEM_NAME}
+ {' '}
+ {i18n.SYSTEM_DESCRIPTION_PART2}
+
+
+
{i18n.SYSTEM_DESCRIPTION_PART3}
+
+ ),
+ example: SystemExample,
+ searchableDescription: `${i18n.SYSTEM_DESCRIPTION_PART1} ${i18n.SYSTEM_NAME} ${i18n.SYSTEM_DESCRIPTION_PART2} ${i18n.SYSTEM_DESCRIPTION_PART3}`,
+ },
+ {
+ id: RowRendererId.system_endgame_process,
+ name: i18n.PROCESS,
+ description: (
+
+
{i18n.PROCESS_DESCRIPTION_PART1}
+
+
{i18n.PROCESS_DESCRIPTION_PART2}
+
+ ),
+ example: SystemEndgameProcessExample,
+ searchableDescription: `${i18n.PROCESS_DESCRIPTION_PART1} ${i18n.PROCESS_DESCRIPTION_PART2}`,
+ },
+ {
+ id: RowRendererId.system_fim,
+ name: i18n.FIM_NAME,
+ description: i18n.FIM_DESCRIPTION_PART1,
+ example: SystemFimExample,
+ searchableDescription: i18n.FIM_DESCRIPTION_PART1,
+ },
+ {
+ id: RowRendererId.system_file,
+ name: i18n.FILE_NAME,
+ description: i18n.FILE_DESCRIPTION_PART1,
+ example: SystemFileExample,
+ searchableDescription: i18n.FILE_DESCRIPTION_PART1,
+ },
+ {
+ id: RowRendererId.system_socket,
+ name: i18n.SOCKET_NAME,
+ description: (
+
+
{i18n.SOCKET_DESCRIPTION_PART1}
+
+
{i18n.SOCKET_DESCRIPTION_PART2}
+
+ ),
+ example: SystemSocketExample,
+ searchableDescription: `${i18n.SOCKET_DESCRIPTION_PART1} ${i18n.SOCKET_DESCRIPTION_PART2}`,
+ },
+ {
+ id: RowRendererId.suricata,
+ name: 'Suricata',
+ description: (
+
+ {i18n.SURICATA_DESCRIPTION_PART1}{' '}
+
+ {i18n.SURICATA_NAME}
+ {' '}
+ {i18n.SURICATA_DESCRIPTION_PART2}
+
+ ),
+ example: SuricataExample,
+ searchableDescription: `${i18n.SURICATA_DESCRIPTION_PART1} ${i18n.SURICATA_NAME} ${i18n.SURICATA_DESCRIPTION_PART2}`,
+ },
+ {
+ id: RowRendererId.zeek,
+ name: i18n.ZEEK_NAME,
+ description: (
+
+ {i18n.ZEEK_DESCRIPTION_PART1}{' '}
+
+ {i18n.ZEEK_NAME}
+ {' '}
+ {i18n.ZEEK_DESCRIPTION_PART2}
+
+ ),
+ example: ZeekExample,
+ searchableDescription: `${i18n.ZEEK_DESCRIPTION_PART1} ${i18n.ZEEK_NAME} ${i18n.ZEEK_DESCRIPTION_PART2}`,
+ },
+];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts
new file mode 100644
index 0000000000000..f4d473cdfd3d2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts
@@ -0,0 +1,215 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 AUDITD_NAME = i18n.translate('xpack.securitySolution.eventRenderers.auditdName', {
+ defaultMessage: 'Auditd',
+});
+
+export const AUDITD_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.auditdDescriptionPart1',
+ {
+ defaultMessage: 'audit events convey security-relevant logs from the Linux Audit Framework.',
+ }
+);
+
+export const AUDITD_FILE_NAME = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.auditdFileName',
+ {
+ defaultMessage: 'Auditd File',
+ }
+);
+
+export const AUDITD_FILE_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.auditdFileDescriptionPart1',
+ {
+ defaultMessage:
+ 'File events show users (and system accounts) performing CRUD operations on files via specific processes.',
+ }
+);
+
+export const AUTHENTICATION_NAME = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.authenticationName',
+ {
+ defaultMessage: 'Authentication',
+ }
+);
+
+export const AUTHENTICATION_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart1',
+ {
+ defaultMessage:
+ 'Authentication events show users (and system accounts) successfully or unsuccessfully logging into hosts.',
+ }
+);
+
+export const AUTHENTICATION_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart2',
+ {
+ defaultMessage:
+ 'Some authentication events may include additional details when users authenticate on behalf of other users.',
+ }
+);
+
+export const DNS_NAME = i18n.translate('xpack.securitySolution.eventRenderers.dnsName', {
+ defaultMessage: 'Domain Name System (DNS)',
+});
+
+export const DNS_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.dnsDescriptionPart1',
+ {
+ defaultMessage:
+ 'Domain Name System (DNS) events show users (and system accounts) making requests via specific processes to translate from host names to IP addresses.',
+ }
+);
+
+export const FILE_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fileName', {
+ defaultMessage: 'File',
+});
+
+export const FILE_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.fileDescriptionPart1',
+ {
+ defaultMessage:
+ 'File events show users (and system accounts) performing CRUD operations on files via specific processes.',
+ }
+);
+
+export const FIM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fimName', {
+ defaultMessage: 'File Integrity Module (FIM)',
+});
+
+export const FIM_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.fimDescriptionPart1',
+ {
+ defaultMessage:
+ 'File Integrity Module (FIM) events show users (and system accounts) performing CRUD operations on files via specific processes.',
+ }
+);
+
+export const FLOW_NAME = i18n.translate('xpack.securitySolution.eventRenderers.flowName', {
+ defaultMessage: 'Flow',
+});
+
+export const FLOW_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.flowDescriptionPart1',
+ {
+ defaultMessage:
+ "The Flow renderer visualizes the flow of data between a source and destination. It's applicable to many types of events.",
+ }
+);
+
+export const FLOW_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.flowDescriptionPart2',
+ {
+ defaultMessage:
+ 'The hosts, ports, protocol, direction, duration, amount transferred, process, geographic location, and other details are visualized when available.',
+ }
+);
+
+export const PROCESS = i18n.translate('xpack.securitySolution.eventRenderers.processName', {
+ defaultMessage: 'Process',
+});
+
+export const PROCESS_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.processDescriptionPart1',
+ {
+ defaultMessage:
+ 'Process events show users (and system accounts) starting and stopping processes.',
+ }
+);
+
+export const PROCESS_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.processDescriptionPart2',
+ {
+ defaultMessage:
+ 'Details including the command line arguments, parent process, and if applicable, file hashes are displayed when available.',
+ }
+);
+
+export const SOCKET_NAME = i18n.translate('xpack.securitySolution.eventRenderers.socketName', {
+ defaultMessage: 'Socket (Network)',
+});
+
+export const SOCKET_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.socketDescriptionPart1',
+ {
+ defaultMessage:
+ 'Socket (Network) events show processes listening, accepting, and closing connections.',
+ }
+);
+
+export const SOCKET_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.socketDescriptionPart2',
+ {
+ defaultMessage:
+ 'Details including the protocol, ports, and a community ID for correlating all network events related to a single flow are displayed when available.',
+ }
+);
+
+export const SURICATA_NAME = i18n.translate('xpack.securitySolution.eventRenderers.suricataName', {
+ defaultMessage: 'Suricata',
+});
+
+export const SURICATA_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.suricataDescriptionPart1',
+ {
+ defaultMessage: 'Summarizes',
+ }
+);
+
+export const SURICATA_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.suricataDescriptionPart2',
+ {
+ defaultMessage:
+ 'intrusion detection (IDS), inline intrusion prevention (IPS), and network security monitoring (NSM) events',
+ }
+);
+
+export const SYSTEM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.systemName', {
+ defaultMessage: 'System',
+});
+
+export const SYSTEM_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.systemDescriptionPart1',
+ {
+ defaultMessage: 'The Auditbeat',
+ }
+);
+
+export const SYSTEM_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.systemDescriptionPart2',
+ {
+ defaultMessage: 'module collects various security related information about a system.',
+ }
+);
+
+export const SYSTEM_DESCRIPTION_PART3 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.systemDescriptionPart3',
+ {
+ defaultMessage:
+ 'All datasets send both periodic state information (e.g. all currently running processes) and real-time changes (e.g. when a new process starts or stops).',
+ }
+);
+
+export const ZEEK_NAME = i18n.translate('xpack.securitySolution.eventRenderers.zeekName', {
+ defaultMessage: 'Zeek (formerly Bro)',
+});
+
+export const ZEEK_DESCRIPTION_PART1 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.zeekDescriptionPart1',
+ {
+ defaultMessage: 'Summarizes events from the',
+ }
+);
+
+export const ZEEK_DESCRIPTION_PART2 = i18n.translate(
+ 'xpack.securitySolution.eventRenderers.zeekDescriptionPart2',
+ {
+ defaultMessage: 'Network Security Monitoring (NSM) tool',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts
new file mode 100644
index 0000000000000..4749afda9570a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/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 ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID = 'row-renderer-example';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx
new file mode 100644
index 0000000000000..d90d0fdfa558b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
+import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer';
+import { CONNECTED_USING } from '../../timeline/body/renderers/auditd/translations';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const AuditdExampleComponent: React.FC = () => {
+ const auditdRowRenderer = createGenericAuditRowRenderer({
+ actionName: 'connected-to',
+ text: CONNECTED_USING,
+ });
+
+ return (
+ <>
+ {auditdRowRenderer.renderRow({
+ browserFields: {},
+ data: mockTimelineData[26].ecs,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const AuditdExample = React.memo(AuditdExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx
new file mode 100644
index 0000000000000..fc8e51864f50a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
+import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer';
+import { OPENED_FILE, USING } from '../../timeline/body/renderers/auditd/translations';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const AuditdFileExampleComponent: React.FC = () => {
+ const auditdFileRowRenderer = createGenericFileRowRenderer({
+ actionName: 'opened-file',
+ text: `${OPENED_FILE} ${USING}`,
+ });
+
+ return (
+ <>
+ {auditdFileRowRenderer.renderRow({
+ browserFields: {},
+ data: mockTimelineData[27].ecs,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const AuditdFileExample = React.memo(AuditdFileExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx
new file mode 100644
index 0000000000000..3cc39a3bf7050
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './auditd';
+export * from './auditd_file';
+export * from './netflow';
+export * from './suricata';
+export * from './system';
+export * from './system_dns';
+export * from './system_endgame_process';
+export * from './system_file';
+export * from './system_fim';
+export * from './system_security_event';
+export * from './system_socket';
+export * from './zeek';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx
new file mode 100644
index 0000000000000..a276bafb65c60
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { getMockNetflowData } from '../../../../common/mock/netflow';
+import { netflowRowRenderer } from '../../timeline/body/renderers/netflow/netflow_row_renderer';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const NetflowExampleComponent: React.FC = () => (
+ <>
+ {netflowRowRenderer.renderRow({
+ browserFields: {},
+ data: getMockNetflowData(),
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+);
+export const NetflowExample = React.memo(NetflowExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx
new file mode 100644
index 0000000000000..318f427b81f28
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
+import { suricataRowRenderer } from '../../timeline/body/renderers/suricata/suricata_row_renderer';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SuricataExampleComponent: React.FC = () => (
+ <>
+ {suricataRowRenderer.renderRow({
+ browserFields: {},
+ data: mockTimelineData[2].ecs,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+);
+export const SuricataExample = React.memo(SuricataExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx
new file mode 100644
index 0000000000000..c8c3b48ac366a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { TERMINATED_PROCESS } from '../../timeline/body/renderers/system/translations';
+import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemExampleComponent: React.FC = () => {
+ const systemRowRenderer = createGenericSystemRowRenderer({
+ actionName: 'termination_event',
+ text: TERMINATED_PROCESS,
+ });
+
+ return (
+ <>
+ {systemRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameTerminationEvent,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemExample = React.memo(SystemExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx
new file mode 100644
index 0000000000000..4937b0f05ce7c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx
@@ -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 React from 'react';
+
+import { createDnsRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { mockEndgameDnsRequest } from '../../../../common/mock/mock_endgame_ecs_data';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemDnsExampleComponent: React.FC = () => {
+ const systemDnsRowRenderer = createDnsRowRenderer();
+
+ return (
+ <>
+ {systemDnsRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameDnsRequest,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemDnsExample = React.memo(SystemDnsExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx
new file mode 100644
index 0000000000000..675bc172ab6f7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data';
+import { PROCESS_STARTED } from '../../timeline/body/renderers/system/translations';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemEndgameProcessExampleComponent: React.FC = () => {
+ const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({
+ actionName: 'creation_event',
+ text: PROCESS_STARTED,
+ });
+
+ return (
+ <>
+ {systemEndgameProcessRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameCreationEvent,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemEndgameProcessExample = React.memo(SystemEndgameProcessExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx
new file mode 100644
index 0000000000000..62c243a7e8502
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data';
+import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { DELETED_FILE } from '../../timeline/body/renderers/system/translations';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemFileExampleComponent: React.FC = () => {
+ const systemFileRowRenderer = createGenericFileRowRenderer({
+ actionName: 'file_delete_event',
+ text: DELETED_FILE,
+ });
+
+ return (
+ <>
+ {systemFileRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameFileDeleteEvent,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemFileExample = React.memo(SystemFileExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx
new file mode 100644
index 0000000000000..ad3eeb7f797ff
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data';
+import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { CREATED_FILE } from '../../timeline/body/renderers/system/translations';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemFimExampleComponent: React.FC = () => {
+ const systemFimRowRenderer = createFimRowRenderer({
+ actionName: 'file_create_event',
+ text: CREATED_FILE,
+ });
+
+ return (
+ <>
+ {systemFimRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameFileCreateEvent,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemFimExample = React.memo(SystemFimExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx
new file mode 100644
index 0000000000000..bc577771cc90c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.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 { createSecurityEventRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { mockEndgameUserLogon } from '../../../../common/mock/mock_endgame_ecs_data';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemSecurityEventExampleComponent: React.FC = () => {
+ const systemSecurityEventRowRenderer = createSecurityEventRowRenderer({
+ actionName: 'user_logon',
+ });
+
+ return (
+ <>
+ {systemSecurityEventRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameUserLogon,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemSecurityEventExample = React.memo(SystemSecurityEventExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx
new file mode 100644
index 0000000000000..dd119d1b60f39
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ACCEPTED_A_CONNECTION_VIA } from '../../timeline/body/renderers/system/translations';
+import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
+import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const SystemSocketExampleComponent: React.FC = () => {
+ const systemSocketRowRenderer = createSocketRowRenderer({
+ actionName: 'ipv4_connection_accept_event',
+ text: ACCEPTED_A_CONNECTION_VIA,
+ });
+ return (
+ <>
+ {systemSocketRowRenderer.renderRow({
+ browserFields: {},
+ data: mockEndgameIpv4ConnectionAcceptEvent,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+ );
+};
+export const SystemSocketExample = React.memo(SystemSocketExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx
new file mode 100644
index 0000000000000..56f0d207fbc6d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
+import { zeekRowRenderer } from '../../timeline/body/renderers/zeek/zeek_row_renderer';
+import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
+
+const ZeekExampleComponent: React.FC = () => (
+ <>
+ {zeekRowRenderer.renderRow({
+ browserFields: {},
+ data: mockTimelineData[13].ecs,
+ timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
+ })}
+ >
+);
+export const ZeekExample = React.memo(ZeekExampleComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
new file mode 100644
index 0000000000000..2792b264ba7e2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
@@ -0,0 +1,182 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 {
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiText,
+ EuiToolTip,
+ EuiOverlayMask,
+ EuiModal,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiInMemoryTable,
+} from '@elastic/eui';
+import React, { useState, useCallback, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import { State } from '../../../common/store';
+
+import { renderers } from './catalog';
+import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions';
+import { RowRenderersBrowser } from './row_renderers_browser';
+import * as i18n from './translations';
+
+const StyledEuiModal = styled(EuiModal)`
+ margin: 0 auto;
+ max-width: 95vw;
+ min-height: 95vh;
+
+ > .euiModal__flex {
+ max-height: 95vh;
+ }
+`;
+
+const StyledEuiModalBody = styled(EuiModalBody)`
+ .euiModalBody__overflow {
+ display: flex;
+ align-items: stretch;
+ overflow: hidden;
+
+ > div {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ > div:first-child {
+ flex: 0;
+ }
+
+ .euiBasicTable {
+ flex: 1;
+ overflow: auto;
+ }
+ }
+ }
+`;
+
+const StyledEuiOverlayMask = styled(EuiOverlayMask)`
+ z-index: 8001;
+ padding-bottom: 0;
+
+ > div {
+ width: 100%;
+ }
+`;
+
+interface StatefulRowRenderersBrowserProps {
+ timelineId: string;
+}
+
+const StatefulRowRenderersBrowserComponent: React.FC = ({
+ timelineId,
+}) => {
+ const tableRef = useRef>();
+ const dispatch = useDispatch();
+ const excludedRowRendererIds = useSelector(
+ (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || []
+ );
+ const [show, setShow] = useState(false);
+
+ const setExcludedRowRendererIds = useCallback(
+ (payload) =>
+ dispatch(
+ dispatchSetExcludedRowRendererIds({
+ id: timelineId,
+ excludedRowRendererIds: payload,
+ })
+ ),
+ [dispatch, timelineId]
+ );
+
+ const toggleShow = useCallback(() => setShow(!show), [show]);
+
+ const hideFieldBrowser = useCallback(() => setShow(false), []);
+
+ const handleDisableAll = useCallback(() => {
+ // eslint-disable-next-line no-unused-expressions
+ tableRef?.current?.setSelection([]);
+ }, [tableRef]);
+
+ const handleEnableAll = useCallback(() => {
+ // eslint-disable-next-line no-unused-expressions
+ tableRef?.current?.setSelection(renderers);
+ }, [tableRef]);
+
+ return (
+ <>
+
+
+ {i18n.EVENT_RENDERERS_TITLE}
+
+
+
+ {show && (
+
+
+
+
+
+ {i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE}
+ {i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION}
+
+
+
+
+
+ {i18n.DISABLE_ALL}
+
+
+
+
+
+ {i18n.ENABLE_ALL}
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+export const StatefulRowRenderersBrowser = React.memo(StatefulRowRenderersBrowserComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx
new file mode 100644
index 0000000000000..d2b0ad788fdb5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui';
+import React, { useMemo, useCallback } from 'react';
+import { xor, xorBy } from 'lodash/fp';
+import styled from 'styled-components';
+
+import { RowRendererId } from '../../../../common/types/timeline';
+import { renderers, RowRendererOption } from './catalog';
+
+interface RowRenderersBrowserProps {
+ excludedRowRendererIds: RowRendererId[];
+ setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
+ .euiTable {
+ tr > *:last-child {
+ display: none;
+ }
+
+ .euiTableHeaderCellCheckbox > .euiTableCellContent {
+ display: none; // we don't want to display checkbox in the table
+ }
+ }
+`;
+
+const StyledEuiFlexItem = styled(EuiFlexItem)`
+ overflow: auto;
+
+ > div {
+ padding: 0;
+
+ > div {
+ margin: 0;
+ }
+ }
+`;
+
+const ExampleWrapperComponent = (Example?: React.ElementType) => {
+ if (!Example) return;
+
+ return (
+
+
+
+ );
+};
+
+const search = {
+ box: {
+ incremental: true,
+ schema: true,
+ },
+};
+
+/**
+ * Since `searchableDescription` contains raw text to power the Search bar,
+ * this "noop" function ensures it's not actually rendered
+ */
+const renderSearchableDescriptionNoop = () => <>>;
+
+const initialSorting = {
+ sort: {
+ field: 'name',
+ direction: 'asc',
+ },
+};
+
+const StyledNameButton = styled.button`
+ text-align: left;
+`;
+
+const RowRenderersBrowserComponent = React.forwardRef(
+ ({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => {
+ const notExcludedRowRenderers = useMemo(() => {
+ if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return [];
+
+ return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id));
+ }, [excludedRowRendererIds]);
+
+ const handleNameClick = useCallback(
+ (item: RowRendererOption) => () => {
+ const newSelection = xor([item], notExcludedRowRenderers);
+ // @ts-ignore
+ ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions
+ },
+ [notExcludedRowRenderers, ref]
+ );
+
+ const nameColumnRenderCallback = useCallback(
+ (value, item) => (
+
+ {value}
+
+ ),
+ [handleNameClick]
+ );
+
+ const columns = useMemo(
+ () => [
+ {
+ field: 'name',
+ name: 'Name',
+ sortable: true,
+ width: '10%',
+ render: nameColumnRenderCallback,
+ },
+ {
+ field: 'description',
+ name: 'Description',
+ width: '25%',
+ render: (description: React.ReactNode) => description,
+ },
+ {
+ field: 'example',
+ name: 'Example',
+ width: '65%',
+ render: ExampleWrapperComponent,
+ },
+ {
+ field: 'searchableDescription',
+ name: 'Searchable Description',
+ sortable: false,
+ width: '0px',
+ render: renderSearchableDescriptionNoop,
+ },
+ ],
+ [nameColumnRenderCallback]
+ );
+
+ const handleSelectable = useCallback(() => true, []);
+
+ const handleSelectionChange = useCallback(
+ (selection: RowRendererOption[]) => {
+ if (!selection || !selection.length)
+ return setExcludedRowRendererIds(Object.values(RowRendererId));
+
+ const excludedRowRenderers = xorBy('id', renderers, selection);
+
+ setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id));
+ },
+ [setExcludedRowRendererIds]
+ );
+
+ const selectionValue = useMemo(
+ () => ({
+ selectable: handleSelectable,
+ onSelectionChange: handleSelectionChange,
+ initialSelected: notExcludedRowRenderers,
+ }),
+ [handleSelectable, handleSelectionChange, notExcludedRowRenderers]
+ );
+
+ return (
+
+ );
+ }
+);
+
+RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';
+
+export const RowRenderersBrowser = React.memo(RowRenderersBrowserComponent);
+
+RowRenderersBrowser.displayName = 'RowRenderersBrowser';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts
new file mode 100644
index 0000000000000..93874ff3240bf
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const EVENT_RENDERERS_TITLE = i18n.translate(
+ 'xpack.securitySolution.customizeEventRenderers.eventRenderersTitle',
+ {
+ defaultMessage: 'Event Renderers',
+ }
+);
+
+export const CUSTOMIZE_EVENT_RENDERERS_TITLE = i18n.translate(
+ 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersTitle',
+ {
+ defaultMessage: 'Customize Event Renderers',
+ }
+);
+
+export const CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersDescription',
+ {
+ defaultMessage:
+ 'Event Renderers automatically convey the most relevant details in an event to reveal its story',
+ }
+);
+
+export const ENABLE_ALL = i18n.translate(
+ 'xpack.securitySolution.customizeEventRenderers.enableAllRenderersButtonLabel',
+ {
+ defaultMessage: 'Enable all',
+ }
+);
+
+export const DISABLE_ALL = i18n.translate(
+ 'xpack.securitySolution.customizeEventRenderers.disableAllRenderersButtonLabel',
+ {
+ defaultMessage: 'Disable all',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
index 53b018fb00adf..78ee9bdd053b2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
@@ -9,8 +9,10 @@ import { useSelector } from 'react-redux';
import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
+import * as i18n from '../translations';
import { Actions } from '.';
+import { TimelineType } from '../../../../../../common/types/timeline';
jest.mock('react-redux', () => {
const origin = jest.requireActual('react-redux');
@@ -202,6 +204,73 @@ describe('Actions', () => {
expect(toggleShowNotes).toBeCalled();
});
+ test('it renders correct tooltip for NotesButton - timeline', () => {
+ const toggleShowNotes = jest.fn();
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP);
+ });
+
+ test('it renders correct tooltip for NotesButton - timeline template', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ ...mockTimelineModel,
+ timelineType: TimelineType.template,
+ });
+ const toggleShowNotes = jest.fn();
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(
+ i18n.NOTES_DISABLE_TOOLTIP
+ );
+ (useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
+ });
+
test('it does NOT render a pin button when isEventViewer is true', () => {
const onPinClicked = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index 2039307691321..125ba23a5c5a5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -9,6 +9,7 @@ import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elas
import { Note } from '../../../../../common/lib/note';
import { StoreState } from '../../../../../common/store/types';
+import { TimelineType } from '../../../../../../common/types/timeline';
import { TimelineModel } from '../../../../store/timeline/model';
@@ -118,9 +119,9 @@ export const Actions = React.memo(
- {loading && }
-
- {!loading && (
+ {loading ? (
+
+ ) : (
(
status={timeline.status}
timelineType={timeline.timelineType}
toggleShowNotes={toggleShowNotes}
- toolTip={timeline.timelineType ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP}
+ toolTip={
+ timeline.timelineType === TimelineType.template
+ ? i18n.NOTES_DISABLE_TOOLTIP
+ : i18n.NOTES_TOOLTIP
+ }
updateNote={updateNote}
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
index efd99e781d827..2436e71a89b86 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
@@ -8,479 +8,481 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
-
-
-
+ }
+ columnHeaders={
+ Array [
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "@timestamp",
+ "width": 190,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "message",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "event.category",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "event.action",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "host.name",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "source.ip",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "destination.ip",
+ "width": 180,
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "user.name",
+ "width": 180,
+ },
+ ]
+ }
+ data-test-subj="field-browser"
+ height={300}
+ isEventViewer={false}
+ onUpdateColumns={[MockFunction]}
+ timelineId="test"
+ toggleColumn={[MockFunction]}
+ width={900}
+ />
+
+
+
{
@@ -36,12 +36,12 @@ describe('helpers', () => {
});
test('returns the events viewer actions column width when isEventViewer is true', () => {
- expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH);
+ expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH);
});
test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => {
expect(getActionsColumnWidth(true, true)).toEqual(
- EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
+ MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
index c538457431fef..903b17c4e8f15 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
@@ -14,6 +14,7 @@ import {
SHOW_CHECK_BOXES_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
+ MINIMUM_ACTIONS_COLUMN_WIDTH,
} from '../constants';
/** Enriches the column headers with field details from the specified browserFields */
@@ -42,7 +43,14 @@ export const getActionsColumnWidth = (
isEventViewer: boolean,
showCheckboxes = false,
additionalActionWidth = 0
-): number =>
- (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) +
- (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
- additionalActionWidth;
+): number => {
+ const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0;
+ const actionsColumnWidth =
+ checkboxesWidth +
+ (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
+ additionalActionWidth;
+
+ return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth
+ ? actionsColumnWidth
+ : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth;
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
index aa0b8d770f60c..b139aa1a7a9a6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
@@ -18,8 +18,6 @@ import {
DRAG_TYPE_FIELD,
droppableTimelineColumnsPrefix,
} from '../../../../../common/components/drag_and_drop/helpers';
-import { StatefulFieldsBrowser } from '../../../fields_browser';
-import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import {
OnColumnRemoved,
OnColumnResized,
@@ -29,6 +27,9 @@ import {
OnUpdateColumns,
} from '../../events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
+import { StatefulFieldsBrowser } from '../../../fields_browser';
+import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
+import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import {
EventsTh,
EventsThContent,
@@ -170,6 +171,7 @@ export const ColumnHeadersComponent = ({
{showSelectAllCheckbox && (
@@ -185,22 +187,23 @@ export const ColumnHeadersComponent = ({
)}
-
-
-
+
+
+
+
{showEventsSelect && (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
index 5f3fb4fa5113c..6b6ae3c3467b5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/** The minimum (fixed) width of the Actions column */
+export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px;
+
/** The (fixed) width of the Actions column */
export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px;
/**
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
index a450d082cb85d..23f7aad049215 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
@@ -21,7 +21,7 @@ import { Note } from '../../../../../common/lib/note';
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { AssociateNote, UpdateNote } from '../../../notes/helpers';
import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
-import { EventsTdContent, EventsTrData } from '../../styles';
+import { EventsTd, EventsTdContent, EventsTrData } from '../../styles';
import { Actions } from '../actions';
import { DataDrivenColumns } from '../data_driven_columns';
import { eventHasNotes, getPinOnClick } from '../helpers';
@@ -90,8 +90,8 @@ export const EventColumnView = React.memo(
}) => {
const { getManageTimelineById } = useManageTimeline();
const timelineActions = useMemo(
- () => getManageTimelineById(timelineId).timelineRowActions(ecsData),
- [ecsData, getManageTimelineById, timelineId]
+ () => getManageTimelineById(timelineId).timelineRowActions({ nonEcsData: data, ecsData }),
+ [data, ecsData, getManageTimelineById, timelineId]
);
const [isPopoverOpen, setPopover] = useState(false);
@@ -133,22 +133,24 @@ export const EventColumnView = React.memo(
...acc,
icon: [
...acc.icon,
-
-
- action.onClick({ eventId: id, ecsData, data })}
- />
-
- ,
+
+
+
+ action.onClick({ eventId: id, ecsData, data })}
+ />
+
+
+ ,
],
};
}
@@ -176,23 +178,25 @@ export const EventColumnView = React.memo(
return grouped.contextMenu.length > 0
? [
...grouped.icon,
-
-
+
-
-
- ,
+
+
+
+
+ ,
]
: grouped.icon;
}, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
index 7ecd7ec5ed35c..8ba1a999e2b2a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
@@ -223,7 +223,7 @@ describe('helpers', () => {
eventHasNotes: false,
timelineType: TimelineType.template,
})
- ).toEqual('This event cannot be pinned because it is filtered by a timeline template');
+ ).toEqual('This event may not be pinned while editing a template timeline');
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index 8f8f19020697c..b474e4047eadd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -33,6 +33,7 @@ import { Sort } from './sort';
import { useManageTimeline } from '../../manage_timeline';
import { GraphOverlay } from '../../graph_overlay';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
+import { TimelineRowAction } from './actions';
export interface BodyProps {
addNoteToEvent: AddNoteToEvent;
@@ -104,7 +105,18 @@ export const Body = React.memo(
const containerElementRef = useRef(null);
const { getManageTimelineById } = useManageTimeline();
const timelineActions = useMemo(
- () => (data.length > 0 ? getManageTimelineById(id).timelineRowActions(data[0].ecs) : []),
+ () =>
+ data.reduce((acc: TimelineRowAction[], rowData) => {
+ const rowActions = getManageTimelineById(id).timelineRowActions({
+ ecsData: rowData.ecs,
+ nonEcsData: rowData.data,
+ });
+ return rowActions &&
+ rowActions.filter((v) => v.displayType === 'icon').length >
+ acc.filter((v) => v.displayType === 'icon').length
+ ? rowActions
+ : acc;
+ }, []),
[data, getManageTimelineById, id]
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap
index 8e806fadb7bf8..f6fbc771c954a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap
@@ -16,7 +16,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails
processTitle="/lib/systemd/systemd-journald"
result="success"
secondary="root"
- session="unset"
+ session="242"
text="generic-text-123"
userName="root"
workingDirectory="/"
@@ -34,7 +34,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails
"success",
],
"session": Array [
- "unset",
+ "242",
],
"summary": Object {
"actor": Object {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap
index b24a90589ce65..784924e896673 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap
@@ -32,7 +32,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
"message_type": null,
"object": Object {
"primary": Array [
- "93.184.216.34",
+ "192.168.216.34",
],
"secondary": Array [
"80",
@@ -46,7 +46,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
},
"destination": Object {
"ip": Array [
- "93.184.216.34",
+ "192.168.216.34",
],
"port": Array [
80,
@@ -113,7 +113,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
"zeek": null,
}
}
- text="some text"
+ text="connected using"
timelineId="test"
/>
@@ -135,7 +135,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
"success",
],
"session": Array [
- "unset",
+ "242",
],
"summary": Object {
"actor": Object {
@@ -259,7 +259,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
}
}
fileIcon="document"
- text="some text"
+ text="opened file using"
timelineId="test"
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
index aec463f531448..1e314c0ebd281 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
@@ -34,7 +34,7 @@ describe('GenericRowRenderer', () => {
auditd = cloneDeep(mockTimelineData[26].ecs);
connectedToRenderer = createGenericAuditRowRenderer({
actionName: 'connected-to',
- text: 'some text',
+ text: 'connected using',
});
});
test('renders correctly against snapshot', () => {
@@ -80,7 +80,7 @@ describe('GenericRowRenderer', () => {
);
expect(wrapper.text()).toContain(
- 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80'
+ 'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination192.168.216.34:80'
);
});
});
@@ -95,7 +95,7 @@ describe('GenericRowRenderer', () => {
auditdFile = cloneDeep(mockTimelineData[27].ecs);
fileToRenderer = createGenericFileRowRenderer({
actionName: 'opened-file',
- text: 'some text',
+ text: 'opened file using',
});
});
@@ -142,7 +142,7 @@ describe('GenericRowRenderer', () => {
);
expect(wrapper.text()).toContain(
- 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess'
+ 'Session242root@zeek-londonin/opened file using/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess'
);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
index e9d0bdfa3a323..3e7520f641f4a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
@@ -10,6 +10,8 @@ import { IconType } from '@elastic/eui';
import { get } from 'lodash/fp';
import React from 'react';
+import { RowRendererId } from '../../../../../../../common/types/timeline';
+
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { AuditdGenericDetails } from './generic_details';
import { AuditdGenericFileDetails } from './generic_file_details';
@@ -22,6 +24,7 @@ export const createGenericAuditRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.auditd,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@@ -54,6 +57,7 @@ export const createGenericFileRowRenderer = ({
text: string;
fileIcon?: IconType;
}): RowRenderer => ({
+ id: RowRendererId.auditd_file,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
index 91499fd9c30f5..795c914c3c9a0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
@@ -10,6 +10,7 @@ import { get } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
+import { RowRendererId } from '../../../../../../../common/types/timeline';
import { asArrayIfExists } from '../../../../../../common/lib/helpers';
import {
TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME,
@@ -84,6 +85,7 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu
};
export const netflowRowRenderer: RowRenderer = {
+ id: RowRendererId.netflow,
isInstance: (ecs) =>
eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) ||
eventActionMatches(get(EVENT_ACTION_FIELD, ecs)),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
index e63f60226c707..0b860491918df 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
@@ -4,13 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
+import { RowRendererId } from '../../../../../../common/types/timeline';
+
import { RowRenderer } from './row_renderer';
+const PlainRowRenderer = () => <>>;
+
+PlainRowRenderer.displayName = 'PlainRowRenderer';
+
export const plainRowRenderer: RowRenderer = {
+ id: RowRendererId.plain,
isInstance: (_) => true,
- renderRow: () => <>>,
+ renderRow: PlainRowRenderer,
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
index 5cee0a0118dd2..609e9dba1a46e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { BrowserFields } from '../../../../../common/containers/source';
+import type { RowRendererId } from '../../../../../../common/types/timeline';
import { Ecs } from '../../../../../graphql/types';
import { EventsTrSupplement } from '../../styles';
@@ -22,6 +23,7 @@ export const RowRendererContainer = React.memo(({ chi
RowRendererContainer.displayName = 'RowRendererContainer';
export interface RowRenderer {
+ id: RowRendererId;
isInstance: (data: Ecs) => boolean;
renderRow: ({
browserFields,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap
index f766befaf47e4..e55465cfd8895 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap
@@ -34,7 +34,6 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1`
>
Hello
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
index 5012f321188d6..242f63611f2ff 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
@@ -9,10 +9,13 @@
import { get } from 'lodash/fp';
import React from 'react';
+import { RowRendererId } from '../../../../../../../common/types/timeline';
+
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { SuricataDetails } from './suricata_details';
export const suricataRowRenderer: RowRenderer = {
+ id: RowRendererId.suricata,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'suricata';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx
index db0ddd857238f..1cd78178d017f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx
@@ -13,7 +13,6 @@ import {
DraggableWrapper,
} from '../../../../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers';
-import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon';
import { GoogleLink } from '../../../../../../common/components/links';
import { Provider } from '../../../data_providers/provider';
@@ -122,7 +121,6 @@ export const SuricataSignature = React.memo<{
{signature.split(' ').splice(tokens.length).join(' ')}
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
index e31fc26e4ae52..67e050160805e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
@@ -9,6 +9,8 @@
import { get } from 'lodash/fp';
import React from 'react';
+import { RowRendererId } from '../../../../../../../common/types/timeline';
+
import { DnsRequestEventDetails } from '../dns/dns_request_event_details';
import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details';
import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers';
@@ -25,6 +27,7 @@ export const createGenericSystemRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.system,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@@ -55,6 +58,7 @@ export const createEndgameProcessRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.system_file,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
const category: string | null | undefined = get('event.category[0]', ecs);
@@ -86,6 +90,7 @@ export const createFimRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.system_fim,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
const category: string | null | undefined = get('event.category[0]', ecs);
@@ -117,6 +122,7 @@ export const createGenericFileRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.system_file,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@@ -147,6 +153,7 @@ export const createSocketRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
+ id: RowRendererId.system_socket,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
return action != null && action.toLowerCase() === actionName;
@@ -169,6 +176,7 @@ export const createSecurityEventRowRenderer = ({
}: {
actionName: string;
}): RowRenderer => ({
+ id: RowRendererId.system_security_event,
isInstance: (ecs) => {
const category: string | null | undefined = get('event.category[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@@ -192,6 +200,7 @@ export const createSecurityEventRowRenderer = ({
});
export const createDnsRowRenderer = (): RowRenderer => ({
+ id: RowRendererId.system_dns,
isInstance: (ecs) => {
const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs);
const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
index 25228b04bb50b..9bbb7a4090dea 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
@@ -9,10 +9,13 @@
import { get } from 'lodash/fp';
import React from 'react';
+import { RowRendererId } from '../../../../../../../common/types/timeline';
+
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { ZeekDetails } from './zeek_details';
export const zeekRowRenderer: RowRenderer = {
+ id: RowRendererId.zeek,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'zeek';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx
index cdf4a8cba68ab..74f75a0a73386 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx
@@ -15,7 +15,6 @@ import {
DraggableWrapper,
} from '../../../../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers';
-import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon';
import { GoogleLink, ReputationLink } from '../../../../../../common/components/links';
import { Provider } from '../../../data_providers/provider';
import { IS_OPERATOR } from '../../../data_providers/data_provider';
@@ -120,7 +119,6 @@ export const Link = React.memo(({ value, link }) => {
{value}
-
);
@@ -129,7 +127,6 @@ export const Link = React.memo(({ value, link }) => {
-
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
index c9660182a4050..141534f1dcb6f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
@@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
-import { TimelineId } from '../../../../../common/types/timeline';
+import { RowRendererId, TimelineId } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import { TimelineItem } from '../../../../graphql/types';
import { Note } from '../../../../common/lib/note';
@@ -60,6 +60,7 @@ const StatefulBodyComponent = React.memo(
columnHeaders,
data,
eventIdToNoteIds,
+ excludedRowRendererIds,
height,
id,
isEventViewer = false,
@@ -74,7 +75,6 @@ const StatefulBodyComponent = React.memo(
clearSelected,
show,
showCheckboxes,
- showRowRenderers,
graphEventId,
sort,
toggleColumn,
@@ -97,8 +97,7 @@ const StatefulBodyComponent = React.memo(
const onAddNoteToEvent: AddNoteToEvent = useCallback(
({ eventId, noteId }: { eventId: string; noteId: string }) =>
addNoteToEvent!({ id, eventId, noteId }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, addNoteToEvent]
);
const onRowSelected: OnRowSelected = useCallback(
@@ -135,35 +134,36 @@ const StatefulBodyComponent = React.memo(
(sorted) => {
updateSort!({ id, sort: sorted });
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, updateSort]
);
const onColumnRemoved: OnColumnRemoved = useCallback(
(columnId) => removeColumn!({ id, columnId }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, removeColumn]
);
const onColumnResized: OnColumnResized = useCallback(
({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [applyDeltaToColumnWidth, id]
);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [id]);
+ const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [
+ id,
+ pinEvent,
+ ]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [id]);
+ const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [
+ id,
+ unPinEvent,
+ ]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), []);
+ const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [
+ updateNote,
+ ]);
const onUpdateColumns: OnUpdateColumns = useCallback(
(columns) => updateColumns!({ id, columns }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, updateColumns]
);
// Sync to selectAll so parent components can select all events
@@ -171,8 +171,19 @@ const StatefulBodyComponent = React.memo(
if (selectAll) {
onSelectAll({ isSelected: true });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selectAll]); // onSelectAll dependency not necessary
+ }, [onSelectAll, selectAll]);
+
+ const enabledRowRenderers = useMemo(() => {
+ if (
+ excludedRowRendererIds &&
+ excludedRowRendererIds.length === Object.keys(RowRendererId).length
+ )
+ return [plainRowRenderer];
+
+ if (!excludedRowRendererIds) return rowRenderers;
+
+ return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id));
+ }, [excludedRowRendererIds]);
return (
(
onUnPinEvent={onUnPinEvent}
onUpdateColumns={onUpdateColumns}
pinnedEventIds={pinnedEventIds}
- rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]}
+ rowRenderers={enabledRowRenderers}
selectedEventIds={selectedEventIds}
show={id === TimelineId.active ? show : true}
showCheckboxes={showCheckboxes}
@@ -213,6 +224,7 @@ const StatefulBodyComponent = React.memo(
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
deepEqual(prevProps.data, nextProps.data) &&
+ deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds &&
prevProps.graphEventId === nextProps.graphEventId &&
deepEqual(prevProps.notesById, nextProps.notesById) &&
@@ -225,7 +237,6 @@ const StatefulBodyComponent = React.memo(
prevProps.show === nextProps.show &&
prevProps.selectedEventIds === nextProps.selectedEventIds &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
- prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.sort === nextProps.sort
);
@@ -245,6 +256,7 @@ const makeMapStateToProps = () => {
columns,
eventIdToNoteIds,
eventType,
+ excludedRowRendererIds,
graphEventId,
isSelectAllChecked,
loadingEventIds,
@@ -252,13 +264,13 @@ const makeMapStateToProps = () => {
selectedEventIds,
show,
showCheckboxes,
- showRowRenderers,
} = timeline;
return {
columnHeaders: memoizedColumnHeaders(columns, browserFields),
eventIdToNoteIds,
eventType,
+ excludedRowRendererIds,
graphEventId,
isSelectAllChecked,
loadingEventIds,
@@ -268,7 +280,6 @@ const makeMapStateToProps = () => {
selectedEventIds,
show,
showCheckboxes,
- showRowRenderers,
};
};
return mapStateToProps;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
index 5af2f3ef488b0..20467af290b19 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
@@ -9,14 +9,14 @@ import { i18n } from '@kbn/i18n';
export const NOTES_TOOLTIP = i18n.translate(
'xpack.securitySolution.timeline.body.notes.addOrViewNotesForThisEventTooltip',
{
- defaultMessage: 'Add or view notes for this event',
+ defaultMessage: 'Add notes for this event',
}
);
export const NOTES_DISABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.timeline.body.notes.disableEventTooltip',
{
- defaultMessage: 'Add notes for event filtered by a timeline template is not allowed',
+ defaultMessage: 'Notes may not be added here while editing a template timeline',
}
);
@@ -48,7 +48,7 @@ export const PINNED_WITH_NOTES = i18n.translate(
export const DISABLE_PIN = i18n.translate(
'xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip',
{
- defaultMessage: 'This event cannot be pinned because it is filtered by a timeline template',
+ defaultMessage: 'This event may not be pinned while editing a template timeline',
}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
index bc7c313553f1e..ece23d7a10886 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
@@ -97,7 +97,7 @@ export const ProviderItemBadge = React.memo(
useEffect(() => {
// optionally register the provider if provided
- if (!providerRegistered && register != null) {
+ if (register != null) {
dispatch(dragAndDropActions.registerProvider({ provider: { ...register, and: [] } }));
setProviderRegistered(true);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
index 47d848021ba43..eb103d8e7e861 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
@@ -91,10 +91,14 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({
export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thGroupActions ${className}`,
-}))<{ actionsColumnWidth: number }>`
+}))<{ actionsColumnWidth: number; isEventViewer: boolean }>`
display: flex;
- flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
+ flex: 0 0
+ ${({ actionsColumnWidth, isEventViewer }) =>
+ `${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`};
min-width: 0;
+ padding-left: ${({ isEventViewer }) =>
+ !isEventViewer ? '4px;' : '0;'}; // match timeline event border
`;
export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({
@@ -151,6 +155,11 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
width != null
? `${width}px`
: '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+
+ > button.euiButtonIcon,
+ > .euiToolTipAnchor > button.euiButtonIcon {
+ margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`};
+ }
`;
/* EVENTS BODY */
@@ -198,8 +207,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({
}))<{ className: string }>`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
line-height: ${({ theme }) => theme.eui.euiLineHeight};
- padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0
- ${({ theme }) => theme.eui.paddingSizes.xl};
+ padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 52px;
`;
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
@@ -249,6 +257,11 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({
width != null
? `${width}px`
: '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+
+ > button.euiButtonIcon,
+ > .euiToolTipAnchor > button.euiButtonIcon {
+ margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`};
+ }
`;
/**
@@ -334,6 +347,5 @@ export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
*/
export const EventsLoading = styled(EuiLoadingSpinner)`
- margin: ${({ theme }) => theme.eui.euiSizeXS};
- vertical-align: top;
+ vertical-align: middle;
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
index 5cbc922f09c9a..cd03e43938b44 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
@@ -51,6 +51,7 @@ export const allTimelinesQuery = gql`
updatedBy
version
}
+ excludedRowRendererIds
notes {
eventId
note
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
index 4ecabeef16dff..3cf33048007e3 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
@@ -75,6 +75,7 @@ export const getAllTimeline = memoizeOne(
return acc;
}, {})
: null,
+ excludedRowRendererIds: timeline.excludedRowRendererIds,
favorite: timeline.favorite,
noteIds: timeline.noteIds,
notes:
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
index 24beed0801aa6..0aaeb22d72afc 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
@@ -69,6 +69,7 @@ export const oneTimelineQuery = gql`
updatedBy
version
}
+ excludedRowRendererIds
favorite {
fullName
userName
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index 80be2aee80b68..618de48091ce8 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -17,7 +17,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t
import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
import { TimelineNonEcsData } from '../../../graphql/types';
-import { TimelineTypeLiteral } from '../../../../common/types/timeline';
+import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline');
@@ -59,6 +59,7 @@ export const createTimeline = actionCreator<{
start: number;
end: number;
};
+ excludedRowRendererIds?: RowRendererId[];
filters?: Filter[];
columns: ColumnHeaderOptions[];
itemsPerPage?: number;
@@ -69,7 +70,6 @@ export const createTimeline = actionCreator<{
show?: boolean;
sort?: Sort;
showCheckboxes?: boolean;
- showRowRenderers?: boolean;
timelineType?: TimelineTypeLiteral;
templateTimelineId?: string;
templateTimelineVersion?: number;
@@ -266,3 +266,8 @@ export const clearEventsDeleted = actionCreator<{
export const updateEventType = actionCreator<{ id: string; eventType: EventType }>(
'UPDATE_EVENT_TYPE'
);
+
+export const setExcludedRowRendererIds = actionCreator<{
+ id: string;
+ excludedRowRendererIds: RowRendererId[];
+}>('SET_TIMELINE_EXCLUDED_ROW_RENDERER_IDS');
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
index 5290178092f3e..f4c4085715af9 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
@@ -18,6 +18,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick {
description: '',
eventIdToNoteIds: {},
eventType: 'all',
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
filters: [
@@ -146,7 +147,6 @@ describe('Epic Timeline', () => {
selectedEventIds: {},
show: true,
showCheckboxes: false,
- showRowRenderers: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
status: TimelineStatus.active,
width: 1100,
@@ -233,6 +233,7 @@ describe('Epic Timeline', () => {
},
description: '',
eventType: 'all',
+ excludedRowRendererIds: [],
filters: [
{
exists: null,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index 605700cb71a2a..2f9331ec9db8e 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -331,6 +331,7 @@ const timelineInput: TimelineInput = {
dataProviders: null,
description: null,
eventType: null,
+ excludedRowRendererIds: null,
filters: null,
kqlMode: null,
kqlQuery: null,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
index b3d1db23ffae8..632525750c8d8 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
@@ -16,6 +16,7 @@ import {
removeColumn,
upsertColumn,
applyDeltaToColumnWidth,
+ setExcludedRowRendererIds,
updateColumns,
updateItemsPerPage,
updateSort,
@@ -30,6 +31,7 @@ const timelineActionTypes = [
updateColumns.type,
updateItemsPerPage.type,
updateSort.type,
+ setExcludedRowRendererIds.type,
];
export const isPageTimeline = (timelineId: string | undefined): boolean =>
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index a347d3e41e206..59f47297b1f65 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -21,7 +21,11 @@ import {
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../graphql/types';
-import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline';
+import {
+ TimelineTypeLiteral,
+ TimelineType,
+ RowRendererId,
+} from '../../../../common/types/timeline';
import { timelineDefaults } from './defaults';
import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model';
@@ -130,6 +134,7 @@ interface AddNewTimelineParams {
start: number;
end: number;
};
+ excludedRowRendererIds?: RowRendererId[];
filters?: Filter[];
id: string;
itemsPerPage?: number;
@@ -140,7 +145,6 @@ interface AddNewTimelineParams {
show?: boolean;
sort?: Sort;
showCheckboxes?: boolean;
- showRowRenderers?: boolean;
timelineById: TimelineById;
timelineType: TimelineTypeLiteral;
}
@@ -150,6 +154,7 @@ export const addNewTimeline = ({
columns,
dataProviders = [],
dateRange = { start: 0, end: 0 },
+ excludedRowRendererIds = [],
filters = timelineDefaults.filters,
id,
itemsPerPage = timelineDefaults.itemsPerPage,
@@ -157,7 +162,6 @@ export const addNewTimeline = ({
sort = timelineDefaults.sort,
show = false,
showCheckboxes = false,
- showRowRenderers = true,
timelineById,
timelineType,
}: AddNewTimelineParams): TimelineById => {
@@ -176,6 +180,7 @@ export const addNewTimeline = ({
columns,
dataProviders,
dateRange,
+ excludedRowRendererIds,
filters,
itemsPerPage,
kqlQuery,
@@ -186,7 +191,6 @@ export const addNewTimeline = ({
isSaving: false,
isLoading: false,
showCheckboxes,
- showRowRenderers,
timelineType,
...templateTimelineInfo,
},
@@ -1436,3 +1440,25 @@ export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams
},
};
};
+
+interface UpdateExcludedRowRenderersIds {
+ id: string;
+ excludedRowRendererIds: RowRendererId[];
+ timelineById: TimelineById;
+}
+
+export const updateExcludedRowRenderersIds = ({
+ id,
+ excludedRowRendererIds,
+ timelineById,
+}: UpdateExcludedRowRenderersIds): TimelineById => {
+ const timeline = timelineById[id];
+
+ return {
+ ...timelineById,
+ [id]: {
+ ...timeline,
+ excludedRowRendererIds,
+ },
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index a78fbc41ac430..95d525c7eb59f 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -15,6 +15,7 @@ import {
TimelineStatus,
} from '../../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types';
+import type { RowRendererId } from '../../../../common/types/timeline';
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
export type KqlMode = 'filter' | 'search';
@@ -54,6 +55,8 @@ export interface TimelineModel {
eventType?: EventType;
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record;
+ /** A list of Ids of excluded Row Renderers */
+ excludedRowRendererIds: RowRendererId[];
filters?: Filter[];
/** When non-empty, display a graph view for this event */
graphEventId?: string;
@@ -108,8 +111,6 @@ export interface TimelineModel {
show: boolean;
/** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/
showCheckboxes: boolean;
- /** When true, shows additional rowRenderers below the PlainRowRenderer **/
- showRowRenderers: boolean;
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
sort: Sort;
/** status: active | draft */
@@ -131,6 +132,7 @@ export type SubsetTimelineModel = Readonly<
| 'description'
| 'eventType'
| 'eventIdToNoteIds'
+ | 'excludedRowRendererIds'
| 'graphEventId'
| 'highlightedDropAndProviderId'
| 'historyIds'
@@ -153,7 +155,6 @@ export type SubsetTimelineModel = Readonly<
| 'selectedEventIds'
| 'show'
| 'showCheckboxes'
- | 'showRowRenderers'
| 'sort'
| 'width'
| 'isSaving'
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index b8bdb4f2ad7f0..4cfc20eb81705 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -70,6 +70,7 @@ const timelineByIdMock: TimelineById = {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
@@ -97,7 +98,6 @@ const timelineByIdMock: TimelineById = {
selectedEventIds: {},
show: true,
showCheckboxes: false,
- showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: Direction.desc,
@@ -1119,6 +1119,7 @@ describe('Timeline', () => {
deletedEventIds: [],
description: '',
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1139,7 +1140,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1215,6 +1215,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1235,7 +1236,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1421,6 +1421,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1441,7 +1442,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1517,6 +1517,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1537,7 +1538,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1619,6 +1619,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1639,7 +1640,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1722,6 +1722,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1742,7 +1743,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1917,6 +1917,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -1937,7 +1938,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@@ -1995,6 +1995,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@@ -2003,7 +2004,6 @@ describe('Timeline', () => {
isLoading: false,
id: 'foo',
savedObjectId: null,
- showRowRenderers: true,
kqlMode: 'filter',
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
@@ -2099,6 +2099,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
+ excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
@@ -2121,7 +2122,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 6bb546c16b617..d15bce5e217fa 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -25,6 +25,7 @@ import {
removeProvider,
setEventsDeleted,
setEventsLoading,
+ setExcludedRowRendererIds,
setFilters,
setInsertTimeline,
setKqlFilterQueryDraft,
@@ -75,6 +76,7 @@ import {
setLoadingTimelineEvents,
setSelectedTimelineEvents,
unPinTimelineEvent,
+ updateExcludedRowRenderersIds,
updateHighlightedDropAndProvider,
updateKqlFilterQueryDraft,
updateTimelineColumns,
@@ -129,13 +131,13 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
id,
dataProviders,
dateRange,
+ excludedRowRendererIds,
show,
columns,
itemsPerPage,
kqlQuery,
sort,
showCheckboxes,
- showRowRenderers,
timelineType = TimelineType.default,
filters,
}
@@ -146,6 +148,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
columns,
dataProviders,
dateRange,
+ excludedRowRendererIds,
filters,
id,
itemsPerPage,
@@ -153,7 +156,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
sort,
show,
showCheckboxes,
- showRowRenderers,
timelineById: state.timelineById,
timelineType,
}),
@@ -306,6 +308,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
+ .case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({
+ ...state,
+ timelineById: updateExcludedRowRenderersIds({
+ id,
+ excludedRowRendererIds,
+ timelineById: state.timelineById,
+ }),
+ }))
.case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({
...state,
timelineById: setSelectedTimelineEvents({
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
new file mode 100644
index 0000000000000..bb035a19f33d6
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { loggerMock } from 'src/core/server/logging/logger.mock';
+import { createNewPackageConfigMock } from '../../../ingest_manager/common/mocks';
+import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
+import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock';
+import { getPackageConfigCreateCallback } from './ingest_integration';
+
+describe('ingest_integration tests ', () => {
+ describe('ingest_integration sanity checks', () => {
+ test('policy is updated with manifest', async () => {
+ const logger = loggerMock.create();
+ const manifestManager = getManifestManagerMock();
+ const callback = getPackageConfigCreateCallback(logger, manifestManager);
+ const policyConfig = createNewPackageConfigMock();
+ const newPolicyConfig = await callback(policyConfig);
+ expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({
+ artifacts: {
+ 'endpoint-exceptionlist-linux-v1': {
+ compression_algorithm: 'zlib',
+ decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
+ decoded_size: 287,
+ encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c',
+ encoded_size: 133,
+ encryption_algorithm: 'none',
+ relative_url:
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
+ },
+ },
+ manifest_version: 'WzAsMF0=',
+ schema_version: 'v1',
+ });
+ });
+
+ test('policy is returned even if error is encountered during artifact sync', async () => {
+ const logger = loggerMock.create();
+ const manifestManager = getManifestManagerMock();
+ manifestManager.syncArtifacts = jest.fn().mockRejectedValue([new Error('error updating')]);
+ const lastDispatched = await manifestManager.getLastDispatchedManifest();
+ const callback = getPackageConfigCreateCallback(logger, manifestManager);
+ const policyConfig = createNewPackageConfigMock();
+ const newPolicyConfig = await callback(policyConfig);
+ expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
+ lastDispatched.toEndpointFormat()
+ );
+ });
+
+ test('initial policy creation succeeds if snapshot retrieval fails', async () => {
+ const logger = loggerMock.create();
+ const manifestManager = getManifestManagerMock();
+ const lastDispatched = await manifestManager.getLastDispatchedManifest();
+ manifestManager.getSnapshot = jest.fn().mockResolvedValue(null);
+ const callback = getPackageConfigCreateCallback(logger, manifestManager);
+ const policyConfig = createNewPackageConfigMock();
+ const newPolicyConfig = await callback(policyConfig);
+ expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
+ lastDispatched.toEndpointFormat()
+ );
+ });
+
+ test('subsequent policy creations succeed', async () => {
+ const logger = loggerMock.create();
+ const manifestManager = getManifestManagerMock();
+ const snapshot = await manifestManager.getSnapshot();
+ manifestManager.getLastDispatchedManifest = jest.fn().mockResolvedValue(snapshot!.manifest);
+ manifestManager.getSnapshot = jest.fn().mockResolvedValue({
+ manifest: snapshot!.manifest,
+ diffs: [],
+ });
+ const callback = getPackageConfigCreateCallback(logger, manifestManager);
+ const policyConfig = createNewPackageConfigMock();
+ const newPolicyConfig = await callback(policyConfig);
+ expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
+ snapshot!.manifest.toEndpointFormat()
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
index 1acec1e7c53ac..e2522ac4af778 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
@@ -8,7 +8,9 @@ import { Logger } from '../../../../../src/core/server';
import { NewPackageConfig } from '../../../ingest_manager/common/types/models';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
-import { ManifestManager } from './services/artifacts';
+import { ManifestManager, ManifestSnapshot } from './services/artifacts';
+import { reportErrors, ManifestConstants } from './lib/artifacts/common';
+import { ManifestSchemaVersion } from '../../common/endpoint/schema/common';
/**
* Callback to handle creation of PackageConfigs in Ingest Manager
@@ -29,58 +31,83 @@ export const getPackageConfigCreateCallback = (
// follow the types/schema expected
let updatedPackageConfig = newPackageConfig as NewPolicyData;
- // get snapshot based on exception-list-agnostic SOs
- // with diffs from last dispatched manifest, if it exists
- const snapshot = await manifestManager.getSnapshot({ initialize: true });
+ // get current manifest from SO (last dispatched)
+ const manifest = (
+ await manifestManager.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION)
+ )?.toEndpointFormat() ?? {
+ manifest_version: 'default',
+ schema_version: ManifestConstants.SCHEMA_VERSION as ManifestSchemaVersion,
+ artifacts: {},
+ };
- if (snapshot === null) {
- logger.warn('No manifest snapshot available.');
- return updatedPackageConfig;
+ // Until we get the Default Policy Configuration in the Endpoint package,
+ // we will add it here manually at creation time.
+ if (newPackageConfig.inputs.length === 0) {
+ updatedPackageConfig = {
+ ...newPackageConfig,
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ artifact_manifest: {
+ value: manifest,
+ },
+ policy: {
+ value: policyConfigFactory(),
+ },
+ },
+ },
+ ],
+ };
}
- if (snapshot.diffs.length > 0) {
- // create new artifacts
- await manifestManager.syncArtifacts(snapshot, 'add');
+ let snapshot: ManifestSnapshot | null = null;
+ let success = true;
+ try {
+ // Try to get most up-to-date manifest data.
- // Until we get the Default Policy Configuration in the Endpoint package,
- // we will add it here manually at creation time.
- // @ts-ignore
- if (newPackageConfig.inputs.length === 0) {
- updatedPackageConfig = {
- ...newPackageConfig,
- inputs: [
- {
- type: 'endpoint',
- enabled: true,
- streams: [],
- config: {
- artifact_manifest: {
- value: snapshot.manifest.toEndpointFormat(),
- },
- policy: {
- value: policyConfigFactory(),
- },
- },
- },
- ],
+ // get snapshot based on exception-list-agnostic SOs
+ // with diffs from last dispatched manifest, if it exists
+ snapshot = await manifestManager.getSnapshot({ initialize: true });
+
+ if (snapshot && snapshot.diffs.length) {
+ // create new artifacts
+ const errors = await manifestManager.syncArtifacts(snapshot, 'add');
+ if (errors.length) {
+ reportErrors(logger, errors);
+ throw new Error('Error writing new artifacts.');
+ }
+ }
+
+ if (snapshot) {
+ updatedPackageConfig.inputs[0].config.artifact_manifest = {
+ value: snapshot.manifest.toEndpointFormat(),
};
}
- }
- try {
+ return updatedPackageConfig;
+ } catch (err) {
+ success = false;
+ logger.error(err);
return updatedPackageConfig;
} finally {
- if (snapshot.diffs.length > 0) {
- // TODO: let's revisit the way this callback happens... use promises?
- // only commit when we know the package config was created
+ if (success && snapshot !== null) {
try {
- await manifestManager.commit(snapshot.manifest);
+ if (snapshot.diffs.length > 0) {
+ // TODO: let's revisit the way this callback happens... use promises?
+ // only commit when we know the package config was created
+ await manifestManager.commit(snapshot.manifest);
- // clean up old artifacts
- await manifestManager.syncArtifacts(snapshot, 'delete');
+ // clean up old artifacts
+ await manifestManager.syncArtifacts(snapshot, 'delete');
+ }
} catch (err) {
logger.error(err);
}
+ } else if (snapshot === null) {
+ logger.error('No manifest snapshot available.');
}
}
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
index 9ad4554b30203..71d14eb1226d5 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.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 { Logger } from 'src/core/server';
export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
@@ -16,3 +17,9 @@ export const ManifestConstants = {
SCHEMA_VERSION: 'v1',
INITIAL_VERSION: 'WzAsMF0=',
};
+
+export const reportErrors = (logger: Logger, errors: Error[]) => {
+ errors.forEach((err) => {
+ logger.error(err);
+ });
+};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
index acde455f77cb4..1a19306b2fd60 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
@@ -139,6 +139,139 @@ describe('buildEventTypeSignal', () => {
});
});
+ test('it should deduplicate exception entries', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
+ { field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
+ { field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
+ {
+ field: 'host.hostname.text',
+ operator: 'included',
+ type: 'match_any',
+ value: ['estc', 'kibana'],
+ },
+ ];
+
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'server.domain',
+ operator: 'included',
+ type: 'exact_caseless',
+ value: 'DOMAIN',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ {
+ field: 'host.hostname',
+ operator: 'included',
+ type: 'exact_caseless_any',
+ value: ['estc', 'kibana'],
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should not deduplicate exception entries across nested boundaries', async () => {
+ const testEntries: EntriesArray = [
+ {
+ entries: [
+ { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ // Same as above but not inside the nest
+ { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
+ ];
+
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should deduplicate exception items', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
+ ];
+
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'server.domain',
+ operator: 'included',
+ type: 'exact_caseless',
+ value: 'DOMAIN',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+
+ // Create a second exception item with the same entries
+ first.data[1] = getExceptionListItemSchemaMock();
+ first.data[1].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
test('it should ignore unsupported entries', async () => {
// Lists and exists are not supported by the Endpoint
const testEntries: EntriesArray = [
@@ -178,8 +311,9 @@ describe('buildEventTypeSignal', () => {
});
test('it should convert the exception lists response to the proper endpoint format while paging', async () => {
- // The first call returns one exception
+ // The first call returns two exceptions
const first = getFoundExceptionListItemSchemaMock();
+ first.data.push(getExceptionListItemSchemaMock());
// The second call returns two exceptions
const second = getFoundExceptionListItemSchemaMock();
@@ -194,7 +328,8 @@ describe('buildEventTypeSignal', () => {
.mockReturnValueOnce(second)
.mockReturnValueOnce(third);
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
- expect(resp.entries.length).toEqual(3);
+ // Expect 2 exceptions, the first two calls returned the same exception list items
+ expect(resp.entries.length).toEqual(2);
});
test('it should handle no exceptions', async () => {
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index 556405adff62f..b756c4e3d14c3 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -97,10 +97,18 @@ export function translateToEndpointExceptions(
exc: FoundExceptionListItemSchema,
schemaVersion: string
): TranslatedExceptionListItem[] {
+ const entrySet = new Set();
+ const entriesFiltered: TranslatedExceptionListItem[] = [];
if (schemaVersion === 'v1') {
- return exc.data.map((item) => {
- return translateItem(schemaVersion, item);
+ exc.data.forEach((entry) => {
+ const translatedItem = translateItem(schemaVersion, entry);
+ const entryHash = createHash('sha256').update(JSON.stringify(translatedItem)).digest('hex');
+ if (!entrySet.has(entryHash)) {
+ entriesFiltered.push(translatedItem);
+ entrySet.add(entryHash);
+ }
});
+ return entriesFiltered;
} else {
throw new Error('unsupported schemaVersion');
}
@@ -124,12 +132,17 @@ function translateItem(
schemaVersion: string,
item: ExceptionListItemSchema
): TranslatedExceptionListItem {
+ const itemSet = new Set();
return {
type: item.type,
entries: item.entries.reduce((translatedEntries: TranslatedEntry[], entry) => {
const translatedEntry = translateEntry(schemaVersion, entry);
if (translatedEntry !== undefined && translatedEntryType.is(translatedEntry)) {
- translatedEntries.push(translatedEntry);
+ const itemHash = createHash('sha256').update(JSON.stringify(translatedEntry)).digest('hex');
+ if (!itemSet.has(itemHash)) {
+ translatedEntries.push(translatedEntry);
+ itemSet.add(itemHash);
+ }
}
return translatedEntries;
}, []),
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
index aa7f56e815d58..583f4499f591b 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
@@ -11,6 +11,7 @@ import {
TaskManagerStartContract,
} from '../../../../../task_manager/server';
import { EndpointAppContext } from '../../types';
+import { reportErrors } from './common';
export const ManifestTaskConstants = {
TIMEOUT: '1m',
@@ -88,19 +89,36 @@ export class ManifestTask {
return;
}
+ let errors: Error[] = [];
try {
// get snapshot based on exception-list-agnostic SOs
// with diffs from last dispatched manifest
const snapshot = await manifestManager.getSnapshot();
if (snapshot && snapshot.diffs.length > 0) {
// create new artifacts
- await manifestManager.syncArtifacts(snapshot, 'add');
+ errors = await manifestManager.syncArtifacts(snapshot, 'add');
+ if (errors.length) {
+ reportErrors(this.logger, errors);
+ throw new Error('Error writing new artifacts.');
+ }
// write to ingest-manager package config
- await manifestManager.dispatch(snapshot.manifest);
+ errors = await manifestManager.dispatch(snapshot.manifest);
+ if (errors.length) {
+ reportErrors(this.logger, errors);
+ throw new Error('Error dispatching manifest.');
+ }
// commit latest manifest state to user-artifact-manifest SO
- await manifestManager.commit(snapshot.manifest);
+ const error = await manifestManager.commit(snapshot.manifest);
+ if (error) {
+ reportErrors(this.logger, [error]);
+ throw new Error('Error committing manifest.');
+ }
// clean up old artifacts
- await manifestManager.syncArtifacts(snapshot, 'delete');
+ errors = await manifestManager.syncArtifacts(snapshot, 'delete');
+ if (errors.length) {
+ reportErrors(this.logger, errors);
+ throw new Error('Error cleaning up outdated artifacts.');
+ }
}
} catch (err) {
this.logger.error(err);
diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
index 55d7baec36dc6..6a8c26e08d9dd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
@@ -6,6 +6,8 @@
import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { loggerMock } from 'src/core/server/logging/logger.mock';
import { xpackMocks } from '../../../../mocks';
import {
AgentService,
@@ -63,8 +65,8 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
> => {
return {
agentService: createMockAgentService(),
+ logger: loggerMock.create(),
savedObjectsStart: savedObjectsServiceMock.createStartContract(),
- // @ts-ignore
manifestManager: getManifestManagerMock(),
registerIngestCallback: jest.fn<
ReturnType,
diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts
index b7f99fe6fe297..ed97d04eecee6 100644
--- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts
@@ -7,38 +7,38 @@
import * as t from 'io-ts';
import { operator } from '../../../../../lists/common/schemas';
+export const translatedEntryMatchAnyMatcher = t.keyof({
+ exact_cased_any: null,
+ exact_caseless_any: null,
+});
+export type TranslatedEntryMatchAnyMatcher = t.TypeOf;
+
export const translatedEntryMatchAny = t.exact(
t.type({
field: t.string,
operator,
- type: t.keyof({
- exact_cased_any: null,
- exact_caseless_any: null,
- }),
+ type: translatedEntryMatchAnyMatcher,
value: t.array(t.string),
})
);
export type TranslatedEntryMatchAny = t.TypeOf;
-export const translatedEntryMatchAnyMatcher = translatedEntryMatchAny.type.props.type;
-export type TranslatedEntryMatchAnyMatcher = t.TypeOf;
+export const translatedEntryMatchMatcher = t.keyof({
+ exact_cased: null,
+ exact_caseless: null,
+});
+export type TranslatedEntryMatchMatcher = t.TypeOf;
export const translatedEntryMatch = t.exact(
t.type({
field: t.string,
operator,
- type: t.keyof({
- exact_cased: null,
- exact_caseless: null,
- }),
+ type: translatedEntryMatchMatcher,
value: t.string,
})
);
export type TranslatedEntryMatch = t.TypeOf;
-export const translatedEntryMatchMatcher = translatedEntryMatch.type.props.type;
-export type TranslatedEntryMatchMatcher = t.TypeOf;
-
export const translatedEntryMatcher = t.union([
translatedEntryMatchMatcher,
translatedEntryMatchAnyMatcher,
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
index dfbe2572076d0..3bdc5dfbcbd45 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
@@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line max-classes-per-file
import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
import { Logger } from 'src/core/server';
+import { createPackageConfigMock } from '../../../../../../ingest_manager/common/mocks';
+import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server';
+import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { listMock } from '../../../../../../lists/server/mocks';
import {
@@ -21,40 +23,6 @@ import { getArtifactClientMock } from '../artifact_client.mock';
import { getManifestClientMock } from '../manifest_client.mock';
import { ManifestManager } from './manifest_manager';
-function getMockPackageConfig() {
- return {
- id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1',
- inputs: [
- {
- config: {},
- },
- ],
- revision: 1,
- version: 'abcd', // TODO: not yet implemented in ingest_manager (https://github.com/elastic/kibana/issues/69992)
- updated_at: '2020-06-25T16:03:38.159292',
- updated_by: 'kibana',
- created_at: '2020-06-25T16:03:38.159292',
- created_by: 'kibana',
- };
-}
-
-class PackageConfigServiceMock {
- public create = jest.fn().mockResolvedValue(getMockPackageConfig());
- public get = jest.fn().mockResolvedValue(getMockPackageConfig());
- public getByIds = jest.fn().mockResolvedValue([getMockPackageConfig()]);
- public list = jest.fn().mockResolvedValue({
- items: [getMockPackageConfig()],
- total: 1,
- page: 1,
- perPage: 20,
- });
- public update = jest.fn().mockResolvedValue(getMockPackageConfig());
-}
-
-export function getPackageConfigServiceMock() {
- return new PackageConfigServiceMock();
-}
-
async function mockBuildExceptionListArtifacts(
os: string,
schemaVersion: string
@@ -66,27 +34,23 @@ async function mockBuildExceptionListArtifacts(
return [await buildArtifact(exceptions, os, schemaVersion)];
}
-// @ts-ignore
export class ManifestManagerMock extends ManifestManager {
- // @ts-ignore
- private buildExceptionListArtifacts = async () => {
- return mockBuildExceptionListArtifacts('linux', 'v1');
- };
+ protected buildExceptionListArtifacts = jest
+ .fn()
+ .mockResolvedValue(mockBuildExceptionListArtifacts('linux', 'v1'));
- // @ts-ignore
- private getLastDispatchedManifest = jest
+ public getLastDispatchedManifest = jest
.fn()
.mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION));
- // @ts-ignore
- private getManifestClient = jest
+ protected getManifestClient = jest
.fn()
.mockReturnValue(getManifestClientMock(this.savedObjectsClient));
}
export const getManifestManagerMock = (opts?: {
cache?: ExceptionsCache;
- packageConfigService?: PackageConfigServiceMock;
+ packageConfigService?: jest.Mocked;
savedObjectsClient?: ReturnType;
}): ManifestManagerMock => {
let cache = new ExceptionsCache(5);
@@ -94,10 +58,14 @@ export const getManifestManagerMock = (opts?: {
cache = opts.cache;
}
- let packageConfigService = getPackageConfigServiceMock();
+ let packageConfigService = createPackageConfigServiceMock();
if (opts?.packageConfigService !== undefined) {
packageConfigService = opts.packageConfigService;
}
+ packageConfigService.list = jest.fn().mockResolvedValue({
+ total: 1,
+ items: [createPackageConfigMock()],
+ });
let savedObjectsClient = savedObjectsClientMock.create();
if (opts?.savedObjectsClient !== undefined) {
@@ -107,7 +75,6 @@ export const getManifestManagerMock = (opts?: {
const manifestManager = new ManifestManagerMock({
artifactClient: getArtifactClientMock(savedObjectsClient),
cache,
- // @ts-ignore
packageConfigService,
exceptionListClient: listMock.getExceptionListClient(),
logger: loggingSystemMock.create().get() as jest.Mocked,
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
index b1cbc41459f15..d092e7060f8aa 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
@@ -6,13 +6,14 @@
import { inflateSync } from 'zlib';
import { savedObjectsClientMock } from 'src/core/server/mocks';
+import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks';
import {
ArtifactConstants,
ManifestConstants,
Manifest,
ExceptionsCache,
} from '../../../lib/artifacts';
-import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_manager.mock';
+import { getManifestManagerMock } from './manifest_manager.mock';
describe('manifest_manager', () => {
describe('ManifestManager sanity checks', () => {
@@ -73,15 +74,15 @@ describe('manifest_manager', () => {
});
test('ManifestManager can dispatch manifest', async () => {
- const packageConfigService = getPackageConfigServiceMock();
+ const packageConfigService = createPackageConfigServiceMock();
const manifestManager = getManifestManagerMock({ packageConfigService });
const snapshot = await manifestManager.getSnapshot();
const dispatched = await manifestManager.dispatch(snapshot!.manifest);
- expect(dispatched).toEqual(true);
+ expect(dispatched).toEqual([]);
const entries = snapshot!.manifest.getEntries();
const artifact = Object.values(entries)[0].getArtifact();
expect(
- packageConfigService.update.mock.calls[0][2].inputs[0].config.artifact_manifest.value
+ packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value
).toEqual({
manifest_version: ManifestConstants.INITIAL_VERSION,
schema_version: 'v1',
@@ -115,7 +116,7 @@ describe('manifest_manager', () => {
snapshot!.diffs.push(diff);
const dispatched = await manifestManager.dispatch(snapshot!.manifest);
- expect(dispatched).toEqual(true);
+ expect(dispatched).toEqual([]);
await manifestManager.commit(snapshot!.manifest);
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index b9e289cee62af..c8cad32ab746e 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -61,19 +61,25 @@ export class ManifestManager {
/**
* Gets a ManifestClient for the provided schemaVersion.
*
- * @param schemaVersion
+ * @param schemaVersion The schema version of the manifest.
+ * @returns {ManifestClient} A ManifestClient scoped to the provided schemaVersion.
*/
- private getManifestClient(schemaVersion: string) {
+ protected getManifestClient(schemaVersion: string): ManifestClient {
return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion);
}
/**
* Builds an array of artifacts (one per supported OS) based on the current
- * state of exception-list-agnostic SO's.
+ * state of exception-list-agnostic SOs.
*
- * @param schemaVersion
+ * @param schemaVersion The schema version of the artifact
+ * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs.
+ * @throws Throws/rejects if there are errors building the list.
*/
- private async buildExceptionListArtifacts(schemaVersion: string) {
+ protected async buildExceptionListArtifacts(
+ schemaVersion: string
+ ): Promise {
+ // TODO: should wrap in try/catch?
return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce(
async (acc: Promise, os) => {
const exceptionList = await getFullEndpointExceptionList(
@@ -90,13 +96,75 @@ export class ManifestManager {
);
}
+ /**
+ * Writes new artifact SOs based on provided snapshot.
+ *
+ * @param snapshot A ManifestSnapshot to use for writing the artifacts.
+ * @returns {Promise} Any errors encountered.
+ */
+ private async writeArtifacts(snapshot: ManifestSnapshot): Promise {
+ const errors: Error[] = [];
+ for (const diff of snapshot.diffs) {
+ const artifact = snapshot.manifest.getArtifact(diff.id);
+ if (artifact === undefined) {
+ throw new Error(
+ `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.`
+ );
+ }
+
+ const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64'));
+ artifact.body = compressedArtifact.toString('base64');
+ artifact.encodedSize = compressedArtifact.byteLength;
+ artifact.compressionAlgorithm = 'zlib';
+ artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex');
+
+ try {
+ // Write the artifact SO
+ await this.artifactClient.createArtifact(artifact);
+ // Cache the compressed body of the artifact
+ this.cache.set(diff.id, Buffer.from(artifact.body, 'base64'));
+ } catch (err) {
+ if (err.status === 409) {
+ this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`);
+ } else {
+ // TODO: log error here?
+ errors.push(err);
+ }
+ }
+ }
+ return errors;
+ }
+
+ /**
+ * Deletes old artifact SOs based on provided snapshot.
+ *
+ * @param snapshot A ManifestSnapshot to use for deleting the artifacts.
+ * @returns {Promise} Any errors encountered.
+ */
+ private async deleteArtifacts(snapshot: ManifestSnapshot): Promise {
+ const errors: Error[] = [];
+ for (const diff of snapshot.diffs) {
+ try {
+ // Delete the artifact SO
+ await this.artifactClient.deleteArtifact(diff.id);
+ // TODO: should we delete the cache entry here?
+ this.logger.info(`Cleaned up artifact ${diff.id}`);
+ } catch (err) {
+ errors.push(err);
+ }
+ }
+ return errors;
+ }
+
/**
* Returns the last dispatched manifest based on the current state of the
* user-artifact-manifest SO.
*
- * @param schemaVersion
+ * @param schemaVersion The schema version of the manifest.
+ * @returns {Promise} The last dispatched manifest, or null if does not exist.
+ * @throws Throws/rejects if there is an unexpected error retrieving the manifest.
*/
- private async getLastDispatchedManifest(schemaVersion: string) {
+ public async getLastDispatchedManifest(schemaVersion: string): Promise {
try {
const manifestClient = this.getManifestClient(schemaVersion);
const manifestSo = await manifestClient.getManifest();
@@ -127,9 +195,11 @@ export class ManifestManager {
/**
* Snapshots a manifest based on current state of exception-list-agnostic SOs.
*
- * @param opts TODO
+ * @param opts Optional parameters for snapshot retrieval.
+ * @param opts.initialize Initialize a new Manifest when no manifest SO can be retrieved.
+ * @returns {Promise} A snapshot of the manifest, or null if not initialized.
*/
- public async getSnapshot(opts?: ManifestSnapshotOpts) {
+ public async getSnapshot(opts?: ManifestSnapshotOpts): Promise {
try {
let oldManifest: Manifest | null;
@@ -176,71 +246,39 @@ export class ManifestManager {
* Creates artifacts that do not yet exist and cleans up old artifacts that have been
* superceded by this snapshot.
*
- * Can be filtered to apply one or both operations.
- *
- * @param snapshot
- * @param diffType
+ * @param snapshot A ManifestSnapshot to use for sync.
+ * @returns {Promise} Any errors encountered.
*/
- public async syncArtifacts(snapshot: ManifestSnapshot, diffType?: 'add' | 'delete') {
- const filteredDiffs = snapshot.diffs.reduce((diffs: ManifestDiff[], diff) => {
- if (diff.type === diffType || diffType === undefined) {
- diffs.push(diff);
- } else if (!['add', 'delete'].includes(diff.type)) {
- // TODO: replace with io-ts schema
- throw new Error(`Unsupported diff type: ${diff.type}`);
- }
- return diffs;
- }, []);
-
- const adds = filteredDiffs.filter((diff) => {
- return diff.type === 'add';
+ public async syncArtifacts(
+ snapshot: ManifestSnapshot,
+ diffType: 'add' | 'delete'
+ ): Promise {
+ const filteredDiffs = snapshot.diffs.filter((diff) => {
+ return diff.type === diffType;
});
- const deletes = filteredDiffs.filter((diff) => {
- return diff.type === 'delete';
- });
+ const tmpSnapshot = { ...snapshot };
+ tmpSnapshot.diffs = filteredDiffs;
- for (const diff of adds) {
- const artifact = snapshot.manifest.getArtifact(diff.id);
- if (artifact === undefined) {
- throw new Error(
- `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.`
- );
- }
- const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64'));
- artifact.body = compressedArtifact.toString('base64');
- artifact.encodedSize = compressedArtifact.byteLength;
- artifact.compressionAlgorithm = 'zlib';
- artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex');
-
- try {
- await this.artifactClient.createArtifact(artifact);
- } catch (err) {
- if (err.status === 409) {
- this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`);
- } else {
- throw err;
- }
- }
- // Cache the body of the artifact
- this.cache.set(diff.id, Buffer.from(artifact.body, 'base64'));
+ if (diffType === 'add') {
+ return this.writeArtifacts(tmpSnapshot);
+ } else if (diffType === 'delete') {
+ return this.deleteArtifacts(tmpSnapshot);
}
- for (const diff of deletes) {
- await this.artifactClient.deleteArtifact(diff.id);
- // TODO: should we delete the cache entry here?
- this.logger.info(`Cleaned up artifact ${diff.id}`);
- }
+ return [new Error(`Unsupported diff type: ${diffType}`)];
}
/**
* Dispatches the manifest by writing it to the endpoint package config.
*
+ * @param manifest The Manifest to dispatch.
+ * @returns {Promise} Any errors encountered.
*/
- public async dispatch(manifest: Manifest) {
+ public async dispatch(manifest: Manifest): Promise {
let paging = true;
let page = 1;
- let success = true;
+ const errors: Error[] = [];
while (paging) {
const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, {
@@ -264,13 +302,10 @@ export class ManifestManager {
`Updated package config ${id} with manifest version ${manifest.getVersion()}`
);
} catch (err) {
- success = false;
- this.logger.debug(`Error updating package config ${id}`);
- this.logger.error(err);
+ errors.push(err);
}
} else {
- success = false;
- this.logger.debug(`Package config ${id} has no config.`);
+ errors.push(new Error(`Package config ${id} has no config.`));
}
}
@@ -278,32 +313,38 @@ export class ManifestManager {
page++;
}
- // TODO: revisit success logic
- return success;
+ return errors;
}
/**
* Commits a manifest to indicate that it has been dispatched.
*
- * @param manifest
+ * @param manifest The Manifest to commit.
+ * @returns {Promise} An error if encountered, or null if successful.
*/
- public async commit(manifest: Manifest) {
- const manifestClient = this.getManifestClient(manifest.getSchemaVersion());
-
- // Commit the new manifest
- if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) {
- await manifestClient.createManifest(manifest.toSavedObject());
- } else {
- const version = manifest.getVersion();
- if (version === ManifestConstants.INITIAL_VERSION) {
- throw new Error('Updating existing manifest with baseline version. Bad state.');
+ public async commit(manifest: Manifest): Promise {
+ try {
+ const manifestClient = this.getManifestClient(manifest.getSchemaVersion());
+
+ // Commit the new manifest
+ if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) {
+ await manifestClient.createManifest(manifest.toSavedObject());
+ } else {
+ const version = manifest.getVersion();
+ if (version === ManifestConstants.INITIAL_VERSION) {
+ throw new Error('Updating existing manifest with baseline version. Bad state.');
+ }
+ await manifestClient.updateManifest(manifest.toSavedObject(), {
+ version,
+ });
}
- await manifestClient.updateManifest(manifest.toSavedObject(), {
- version,
- });
+
+ this.logger.info(`Committed manifest ${manifest.getVersion()}`);
+ } catch (err) {
+ return err;
}
- this.logger.info(`Committed manifest ${manifest.getVersion()}`);
+ return null;
}
/**
diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
index e46d3be44dbd1..15e188e281d10 100644
--- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
@@ -147,11 +147,28 @@ export const timelineSchema = gql`
custom
}
+ enum RowRendererId {
+ auditd
+ auditd_file
+ netflow
+ plain
+ suricata
+ system
+ system_dns
+ system_endgame_process
+ system_file
+ system_fim
+ system_security_event
+ system_socket
+ zeek
+ }
+
input TimelineInput {
columns: [ColumnHeaderInput!]
dataProviders: [DataProviderInput!]
description: String
eventType: String
+ excludedRowRendererIds: [RowRendererId!]
filters: [FilterTimelineInput!]
kqlMode: String
kqlQuery: SerializedFilterQueryInput
@@ -252,6 +269,7 @@ export const timelineSchema = gql`
description: String
eventIdToNoteIds: [NoteResult!]
eventType: String
+ excludedRowRendererIds: [RowRendererId!]
favorite: [FavoriteTimelineResult!]
filters: [FilterTimelineResult!]
kqlMode: String
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index 52bb4a9862160..6553f709a7fa7 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -126,6 +126,8 @@ export interface TimelineInput {
eventType?: Maybe;
+ excludedRowRendererIds?: Maybe;
+
filters?: Maybe;
kqlMode?: Maybe;
@@ -351,6 +353,22 @@ export enum DataProviderType {
template = 'template',
}
+export enum RowRendererId {
+ auditd = 'auditd',
+ auditd_file = 'auditd_file',
+ netflow = 'netflow',
+ plain = 'plain',
+ suricata = 'suricata',
+ system = 'system',
+ system_dns = 'system_dns',
+ system_endgame_process = 'system_endgame_process',
+ system_file = 'system_file',
+ system_fim = 'system_fim',
+ system_security_event = 'system_security_event',
+ system_socket = 'system_socket',
+ zeek = 'zeek',
+}
+
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@@ -1963,6 +1981,8 @@ export interface TimelineResult {
eventType?: Maybe;
+ excludedRowRendererIds?: Maybe;
+
favorite?: Maybe;
filters?: Maybe;
@@ -8101,6 +8121,12 @@ export namespace TimelineResultResolvers {
eventType?: EventTypeResolver, TypeParent, TContext>;
+ excludedRowRendererIds?: ExcludedRowRendererIdsResolver<
+ Maybe,
+ TypeParent,
+ TContext
+ >;
+
favorite?: FavoriteResolver, TypeParent, TContext>;
filters?: FiltersResolver, TypeParent, TContext>;
@@ -8184,6 +8210,11 @@ export namespace TimelineResultResolvers {
Parent = TimelineResult,
TContext = SiemContext
> = Resolver;
+ export type ExcludedRowRendererIdsResolver<
+ R = Maybe,
+ Parent = TimelineResult,
+ TContext = SiemContext
+ > = Resolver;
export type FavoriteResolver<
R = Maybe,
Parent = TimelineResult,
diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts
index 06b35213b4713..7b84c531dd376 100644
--- a/x-pack/plugins/security_solution/server/index.ts
+++ b/x-pack/plugins/security_solution/server/index.ts
@@ -54,3 +54,4 @@ export { createBootstrapIndex } from './lib/detection_engine/index/create_bootst
export { getIndexExists } from './lib/detection_engine/index/get_index_exists';
export { buildRouteValidation } from './utils/build_validation/route_validation';
export { transformError, buildSiemResponse } from './lib/detection_engine/routes/utils';
+export { readPrivileges } from './lib/detection_engine/privileges/read_privileges';
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
index eb8f6f5022985..d3d7783dc9385 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
@@ -44,5 +44,7 @@ export const pickSavedTimeline = (
savedTimeline.status = TimelineStatus.active;
}
+ savedTimeline.excludedRowRendererIds = savedTimeline.excludedRowRendererIds ?? [];
+
return savedTimeline;
};
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
index 23090bfc6f0bd..f4b97ac3510cc 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
@@ -181,7 +181,7 @@ const getTimelinesFromObjects = async (
if (myTimeline != null) {
const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId);
const timelinePinnedEventIds = myPinnedEventIds.filter((p) => p.timelineId === timelineId);
- const exportedTimeline = omit('status', myTimeline);
+ const exportedTimeline = omit(['status', 'excludedRowRendererIds'], myTimeline);
return [
...acc,
{
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
index 22b98930f3181..c5ee611dfa27f 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
@@ -135,6 +135,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
eventType: {
type: 'keyword',
},
+ excludedRowRendererIds: {
+ type: 'text',
+ },
favorite: {
properties: {
keySearch: {
diff --git a/x-pack/plugins/snapshot_restore/kibana.json b/x-pack/plugins/snapshot_restore/kibana.json
index df72102e52086..92f3e27d6d5b8 100644
--- a/x-pack/plugins/snapshot_restore/kibana.json
+++ b/x-pack/plugins/snapshot_restore/kibana.json
@@ -13,5 +13,9 @@
"security",
"cloud"
],
- "configPath": ["xpack", "snapshot_restore"]
+ "configPath": ["xpack", "snapshot_restore"],
+ "requiredBundles": [
+ "esUiShared",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json
index 9483cb67392c4..0698535cc15fd 100644
--- a/x-pack/plugins/spaces/kibana.json
+++ b/x-pack/plugins/spaces/kibana.json
@@ -14,5 +14,11 @@
],
"server": true,
"ui": true,
- "extraPublicDirs": ["common"]
+ "extraPublicDirs": ["common"],
+ "requiredBundles": [
+ "kibanaReact",
+ "savedObjectsManagement",
+ "management",
+ "home"
+ ]
}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
index 64ddc4428b515..b0103800d4105 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
@@ -19,6 +19,14 @@ import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mock
import { featuresPluginMock } from '../../../../features/public/mocks';
import { Feature } from '../../../../features/public';
+// To be resolved by EUI team.
+// https://github.com/elastic/eui/issues/3712
+jest.mock('@elastic/eui/lib/components/overlay_mask', () => {
+ return {
+ EuiOverlayMask: (props: any) => {props.children}
,
+ };
+});
+
const space = {
id: 'my-space',
name: 'My Space',
diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json
index 391a95853cc16..d7e7a7fabba4f 100644
--- a/x-pack/plugins/transform/kibana.json
+++ b/x-pack/plugins/transform/kibana.json
@@ -13,5 +13,13 @@
"security",
"usageCollection"
],
- "configPath": ["xpack", "transform"]
+ "configPath": ["xpack", "transform"],
+ "requiredBundles": [
+ "ml",
+ "esUiShared",
+ "discover",
+ "kibanaUtils",
+ "kibanaReact",
+ "savedObjects"
+ ]
}
diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json
index 158cfa100d546..03fef90e8ca7f 100644
--- a/x-pack/plugins/triggers_actions_ui/kibana.json
+++ b/x-pack/plugins/triggers_actions_ui/kibana.json
@@ -6,5 +6,6 @@
"optionalPlugins": ["alerts", "alertingBuiltins"],
"requiredPlugins": ["management", "charts", "data"],
"configPath": ["xpack", "trigger_actions_ui"],
- "extraPublicDirs": ["public/common", "public/common/constants"]
+ "extraPublicDirs": ["public/common", "public/common/constants"],
+ "requiredBundles": ["alerts", "esUiShared"]
}
diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json
index a813903d8b212..108c66505f25c 100644
--- a/x-pack/plugins/ui_actions_enhanced/kibana.json
+++ b/x-pack/plugins/ui_actions_enhanced/kibana.json
@@ -8,5 +8,10 @@
"licensing"
],
"server": false,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "data"
+ ]
}
diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json
index 273036a653aeb..31109dd963ab4 100644
--- a/x-pack/plugins/upgrade_assistant/kibana.json
+++ b/x-pack/plugins/upgrade_assistant/kibana.json
@@ -5,5 +5,6 @@
"ui": true,
"configPath": ["xpack", "upgrade_assistant"],
"requiredPlugins": ["management", "licensing"],
- "optionalPlugins": ["cloud", "usageCollection"]
+ "optionalPlugins": ["cloud", "usageCollection"],
+ "requiredBundles": ["management"]
}
diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json
index 152839836ad99..a057e546e4414 100644
--- a/x-pack/plugins/uptime/kibana.json
+++ b/x-pack/plugins/uptime/kibana.json
@@ -13,5 +13,14 @@
],
"server": true,
"ui": true,
- "version": "8.0.0"
+ "version": "8.0.0",
+ "requiredBundles": [
+ "observability",
+ "kibanaReact",
+ "home",
+ "data",
+ "ml",
+ "apm",
+ "maps"
+ ]
}
diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap
index a5dd1abe31ca4..cf80d2de38b3f 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap
@@ -262,6 +262,9 @@ exports[`DonutChart component passes correct props without errors for valid prop
"visible": false,
},
},
+ "background": Object {
+ "color": "rgba(255, 255, 255, 1)",
+ },
"barSeriesStyle": Object {
"displayValue": Object {
"fill": "rgba(105, 112, 125, 1)",
diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap
index 45706720a5e70..004de391a51a4 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap
@@ -132,13 +132,6 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it
-
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
index 823346db3518a..316188eebf65b 100644
--- 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
@@ -27,6 +27,8 @@ Array [
-
+
+
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
index b5fe5d17312c6..7a8ba3fd32cd9 100644
--- 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
@@ -9,6 +9,12 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { AvailabilityReporting } from '../availability_reporting';
import { StatusTag } from '../location_status_tags';
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
+ return {
+ htmlIdGenerator: () => () => `generated-id`,
+ };
+});
+
describe('AvailabilityReporting component', () => {
let allLocations: StatusTag[];
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx
index bfeaa6085e998..d4aae6b022d9c 100644
--- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx
@@ -10,6 +10,12 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { MonitorLocation } from '../../../../../../common/runtime_types/monitor';
import { LocationStatusTags } from '../index';
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
+ return {
+ htmlIdGenerator: () => () => `generated-id`,
+ };
+});
+
describe('LocationStatusTags component', () => {
let monitorLocations: MonitorLocation[];
diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
index 6667da401d6a9..b711b464418a2 100644
--- a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
@@ -288,7 +288,9 @@ exports[`EmptyState component does not render empty state with appropriate base
-
+
-
+
-
+
-
+
{
+ return {
+ htmlIdGenerator: () => () => `generated-id`,
+ };
+});
+
const testFooPings: Ping[] = [
makePing({
docId: 'foo1',
diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json
index a0482ad00797c..ba6a9bfa5e194 100644
--- a/x-pack/plugins/watcher/kibana.json
+++ b/x-pack/plugins/watcher/kibana.json
@@ -10,5 +10,9 @@
"data"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "esUiShared",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts
index 8dbf6d212d5d2..ce1c9d7c269b7 100644
--- a/x-pack/tasks/build.ts
+++ b/x-pack/tasks/build.ts
@@ -16,7 +16,6 @@ import fancyLog from 'fancy-log';
import chalk from 'chalk';
import { generateNoticeFromSource } from '../../src/dev/notice';
-import { prepareTask } from './prepare';
import { gitInfo } from './helpers/git_info';
import { PKG_NAME } from './helpers/pkg';
import { BUILD_VERSION } from './helpers/build_version';
@@ -78,7 +77,6 @@ async function generateNoticeText() {
export const buildTask = gulp.series(
cleanBuildTask,
reportTask,
- prepareTask,
buildCanvasShareableRuntime,
pluginHelpersBuild,
generateNoticeText
diff --git a/x-pack/tasks/dev.ts b/x-pack/tasks/dev.ts
index f43b67e288561..c454817158700 100644
--- a/x-pack/tasks/dev.ts
+++ b/x-pack/tasks/dev.ts
@@ -7,9 +7,7 @@
import * as pluginHelpers from '@kbn/plugin-helpers';
import gulp from 'gulp';
-import { prepareTask } from './prepare';
-
-export const devTask = gulp.series(prepareTask, async function startKibanaServer() {
+export const devTask = gulp.series(async function startKibanaServer() {
await pluginHelpers.run('start', {
flags: process.argv.slice(3),
});
diff --git a/x-pack/tasks/prepare.ts b/x-pack/tasks/prepare.ts
deleted file mode 100644
index 5c71675d44189..0000000000000
--- a/x-pack/tasks/prepare.ts
+++ /dev/null
@@ -1,25 +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 { ensureAllBrowsersDownloaded } from '../plugins/reporting/server/browsers';
-import { LevelLogger } from '../plugins/reporting/server/lib';
-
-export const prepareTask = async () => {
- // eslint-disable-next-line no-console
- const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message);
- const innerLogger = {
- get: () => innerLogger,
- debug: consoleLogger('debug'),
- info: consoleLogger('info'),
- warn: consoleLogger('warn'),
- trace: consoleLogger('trace'),
- error: consoleLogger('error'),
- fatal: consoleLogger('fatal'),
- log: consoleLogger('log'),
- };
- const levelLogger = new LevelLogger(innerLogger);
- await ensureAllBrowsersDownloaded(levelLogger);
-};
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index 0877fdc949dc4..e3281cfdfa9a3 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -25,6 +25,7 @@ const enabledActionTypes = [
'.server-log',
'.servicenow',
'.jira',
+ '.resilient',
'.slack',
'.webhook',
'test.authorization',
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index f1ac3f91c68db..b8b2cbdc03f39 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -12,6 +12,7 @@ import { ActionType } from '../../../../../../../plugins/actions/server';
import { initPlugin as initPagerduty } from './pagerduty_simulation';
import { initPlugin as initServiceNow } from './servicenow_simulation';
import { initPlugin as initJira } from './jira_simulation';
+import { initPlugin as initResilient } from './resilient_simulation';
export const NAME = 'actions-FTS-external-service-simulators';
@@ -20,6 +21,7 @@ export enum ExternalServiceSimulator {
SERVICENOW = 'servicenow',
SLACK = 'slack',
JIRA = 'jira',
+ RESILIENT = 'resilient',
WEBHOOK = 'webhook',
}
@@ -33,6 +35,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] {
);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`);
+ allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`);
return allPaths;
}
@@ -88,6 +91,7 @@ export class FixturePlugin implements Plugin,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ id: '123',
+ create_date: 1589391874472,
+ });
+ }
+ );
+
+ router.patch(
+ {
+ path: `${path}/rest/orgs/201/incidents/{id}`,
+ options: {
+ authRequired: false,
+ },
+ validate: {},
+ },
+ async function (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ success: true,
+ });
+ }
+ );
+
+ router.get(
+ {
+ path: `${path}/rest/orgs/201/incidents/{id}`,
+ options: {
+ authRequired: false,
+ },
+ validate: {},
+ },
+ async function (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ id: '123',
+ create_date: 1589391874472,
+ inc_last_modified_date: 1589391874472,
+ name: 'title',
+ description: 'description',
+ });
+ }
+ );
+
+ router.post(
+ {
+ path: `${path}/rest/api/2/issue/{id}/comment`,
+ options: {
+ authRequired: false,
+ },
+ validate: {},
+ },
+ async function (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ id: '123',
+ created: '2020-04-27T14:17:45.490Z',
+ });
+ }
+ );
+}
+
+function jsonResponse(
+ res: KibanaResponseFactory,
+ code: number,
+ object: Record = {}
+) {
+ return res.custom>({ body: object, statusCode: code });
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts
new file mode 100644
index 0000000000000..a77e0414a19d4
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts
@@ -0,0 +1,549 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+
+import {
+ getExternalServiceSimulatorPath,
+ ExternalServiceSimulator,
+} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin';
+
+const mapping = [
+ {
+ source: 'title',
+ target: 'name',
+ actionType: 'overwrite',
+ },
+ {
+ source: 'description',
+ target: 'description',
+ actionType: 'overwrite',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ actionType: 'append',
+ },
+];
+
+// eslint-disable-next-line import/no-default-export
+export default function resilientTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+
+ const mockResilient = {
+ config: {
+ apiUrl: 'www.jiraisinkibanaactions.com',
+ orgId: '201',
+ casesConfiguration: { mapping },
+ },
+ secrets: {
+ apiKeyId: 'key',
+ apiKeySecret: 'secret',
+ },
+ params: {
+ subAction: 'pushToService',
+ subActionParams: {
+ savedObjectId: '123',
+ title: 'a title',
+ description: 'a description',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ externalId: null,
+ comments: [
+ {
+ commentId: '456',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ],
+ },
+ },
+ };
+
+ let resilientSimulatorURL: string = '';
+
+ describe('IBM Resilient', () => {
+ before(() => {
+ resilientSimulatorURL = kibanaServer.resolveUrl(
+ getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT)
+ );
+ });
+
+ after(() => esArchiver.unload('empty_kibana'));
+
+ describe('IBM Resilient - Action Creation', () => {
+ it('should return 200 when creating a ibm resilient action successfully', async () => {
+ const { body: createdAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient action',
+ actionTypeId: '.resilient',
+ config: {
+ ...mockResilient.config,
+ apiUrl: resilientSimulatorURL,
+ },
+ secrets: mockResilient.secrets,
+ })
+ .expect(200);
+
+ expect(createdAction).to.eql({
+ id: createdAction.id,
+ isPreconfigured: false,
+ name: 'An IBM Resilient action',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: mockResilient.config.casesConfiguration,
+ },
+ });
+
+ const { body: fetchedAction } = await supertest
+ .get(`/api/actions/action/${createdAction.id}`)
+ .expect(200);
+
+ expect(fetchedAction).to.eql({
+ id: fetchedAction.id,
+ isPreconfigured: false,
+ name: 'An IBM Resilient action',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: mockResilient.config.casesConfiguration,
+ },
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action with no apiUrl', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: { orgId: '201' },
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action with no orgId', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: { apiUrl: resilientSimulatorURL },
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type config: [orgId]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action with a non whitelisted apiUrl', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: 'http://resilient.mynonexistent.com',
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: mockResilient.config.casesConfiguration,
+ },
+ secrets: mockResilient.secrets,
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type config: error configuring connector action: target url "http://resilient.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action without secrets', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: mockResilient.config.casesConfiguration,
+ },
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type secrets: [apiKeyId]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action without casesConfiguration', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ },
+ secrets: mockResilient.secrets,
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action with empty mapping', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: { mapping: [] },
+ },
+ secrets: mockResilient.secrets,
+ })
+ .expect(400)
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ statusCode: 400,
+ error: 'Bad Request',
+ message:
+ 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty',
+ });
+ });
+ });
+
+ it('should respond with a 400 Bad Request when creating a ibm resilient action with wrong actionType', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'An IBM Resilient',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: {
+ mapping: [
+ {
+ source: 'title',
+ target: 'description',
+ actionType: 'non-supported',
+ },
+ ],
+ },
+ },
+ secrets: mockResilient.secrets,
+ })
+ .expect(400);
+ });
+ });
+
+ describe('IBM Resilient - Executor', () => {
+ let simulatedActionId: string;
+ before(async () => {
+ const { body } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'A ibm resilient simulator',
+ actionTypeId: '.resilient',
+ config: {
+ apiUrl: resilientSimulatorURL,
+ orgId: mockResilient.config.orgId,
+ casesConfiguration: mockResilient.config.casesConfiguration,
+ },
+ secrets: mockResilient.secrets,
+ });
+ simulatedActionId = body.id;
+ });
+
+ describe('Validation', () => {
+ it('should handle failing with a simulated success without action', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {},
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`,
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without unsupported action', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: { subAction: 'non-supported' },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without subActionParams', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: { subAction: 'pushToService' },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without savedObjectId', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: { subAction: 'pushToService', subActionParams: {} },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without title', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ savedObjectId: 'success',
+ },
+ },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without createdAt', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ savedObjectId: 'success',
+ title: 'success',
+ },
+ },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without commentId', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ ...mockResilient.params.subActionParams,
+ savedObjectId: 'success',
+ title: 'success',
+ createdAt: 'success',
+ createdBy: { username: 'elastic' },
+ comments: [{}],
+ },
+ },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without comment message', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ ...mockResilient.params.subActionParams,
+ savedObjectId: 'success',
+ title: 'success',
+ createdAt: 'success',
+ createdBy: { username: 'elastic' },
+ comments: [{ commentId: 'success' }],
+ },
+ },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]',
+ });
+ });
+ });
+
+ it('should handle failing with a simulated success without comment.createdAt', async () => {
+ await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ ...mockResilient.params.subActionParams,
+ savedObjectId: 'success',
+ title: 'success',
+ createdAt: 'success',
+ createdBy: { username: 'elastic' },
+ comments: [{ commentId: 'success', comment: 'success' }],
+ },
+ },
+ })
+ .then((resp: any) => {
+ expect(resp.body).to.eql({
+ actionId: simulatedActionId,
+ status: 'error',
+ retry: false,
+ message:
+ 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]',
+ });
+ });
+ });
+ });
+
+ describe('Execution', () => {
+ it('should handle creating an incident without comments', async () => {
+ const { body } = await supertest
+ .post(`/api/actions/action/${simulatedActionId}/_execute`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ params: {
+ ...mockResilient.params,
+ subActionParams: {
+ ...mockResilient.params.subActionParams,
+ comments: [],
+ },
+ },
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ status: 'ok',
+ actionId: simulatedActionId,
+ data: {
+ id: '123',
+ title: '123',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ url: `${resilientSimulatorURL}/#incidents/123`,
+ },
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts
index 18b1714582d13..9cdc0c9fa663e 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts
@@ -16,6 +16,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./builtin_action_types/server_log'));
loadTestFile(require.resolve('./builtin_action_types/servicenow'));
loadTestFile(require.resolve('./builtin_action_types/jira'));
+ loadTestFile(require.resolve('./builtin_action_types/resilient'));
loadTestFile(require.resolve('./builtin_action_types/slack'));
loadTestFile(require.resolve('./builtin_action_types/webhook'));
loadTestFile(require.resolve('./create'));
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 c88b094879ac8..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
@@ -22,8 +22,7 @@ export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const es = getService('es');
- // FLAKY: https://github.com/elastic/kibana/issues/69632
- describe.skip('find_statuses', () => {
+ describe('find_statuses', () => {
beforeEach(async () => {
await createSignalsIndex(supertest);
});
diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts
index 6ad9cf4cd5baf..4b980536d2cd1 100644
--- a/x-pack/test/detection_engine_api_integration/utils.ts
+++ b/x-pack/test/detection_engine_api_integration/utils.ts
@@ -235,40 +235,83 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial =
/**
* Remove all alerts from the .kibana index
+ * This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param es The ElasticSearch handle
*/
-export const deleteAllAlerts = async (es: Client): Promise => {
- await es.deleteByQuery({
- index: '.kibana',
- q: 'type:alert',
- wait_for_completion: true,
- refresh: true,
- body: {},
- });
+export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise => {
+ if (retryCount > 0) {
+ try {
+ await es.deleteByQuery({
+ index: '.kibana',
+ q: 'type:alert',
+ wait_for_completion: true,
+ refresh: true,
+ body: {},
+ });
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(`Failure trying to deleteAllAlerts, retries left are: ${retryCount - 1}`, err);
+ await deleteAllAlerts(es, retryCount - 1);
+ }
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Could not deleteAllAlerts, no retries are left');
+ }
};
/**
* Remove all rules statuses from the .kibana index
+ * This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param es The ElasticSearch handle
*/
-export const deleteAllRulesStatuses = async (es: Client): Promise => {
- await es.deleteByQuery({
- index: '.kibana',
- q: 'type:siem-detection-engine-rule-status',
- wait_for_completion: true,
- refresh: true,
- body: {},
- });
+export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise => {
+ if (retryCount > 0) {
+ try {
+ await es.deleteByQuery({
+ index: '.kibana',
+ q: 'type:siem-detection-engine-rule-status',
+ wait_for_completion: true,
+ refresh: true,
+ body: {},
+ });
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `Failure trying to deleteAllRulesStatuses, retries left are: ${retryCount - 1}`,
+ err
+ );
+ await deleteAllRulesStatuses(es, retryCount - 1);
+ }
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Could not deleteAllRulesStatuses, no retries are left');
+ }
};
/**
* Creates the signals index for use inside of beforeEach blocks of tests
+ * This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param supertest The supertest client library
*/
export const createSignalsIndex = async (
- supertest: SuperTest
+ supertest: SuperTest,
+ retryCount = 20
): Promise => {
- await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send().expect(200);
+ if (retryCount > 0) {
+ try {
+ await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `Failure trying to create the signals index, retries left are: ${retryCount - 1}`,
+ err
+ );
+ await createSignalsIndex(supertest, retryCount - 1);
+ }
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Could not createSignalsIndex, no retries are left');
+ }
};
/**
@@ -276,9 +319,21 @@ export const createSignalsIndex = async (
* @param supertest The supertest client library
*/
export const deleteSignalsIndex = async (
- supertest: SuperTest
+ supertest: SuperTest,
+ retryCount = 20
): Promise => {
- await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send().expect(200);
+ if (retryCount > 0) {
+ try {
+ await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(`Failure trying to deleteSignalsIndex, retries left are: ${retryCount - 1}`, err);
+ await deleteSignalsIndex(supertest, retryCount - 1);
+ }
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Could not deleteSignalsIndex, no retries are left');
+ }
};
/**
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index db33775abeb0a..7207bb3fc37b3 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -116,6 +116,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
version: policyInfo.packageInfo.version,
},
},
+ artifact_manifest: {
+ artifacts: {
+ 'endpoint-exceptionlist-linux-v1': {
+ compression_algorithm: 'zlib',
+ decoded_sha256:
+ 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ decoded_size: 14,
+ encoded_sha256:
+ 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda',
+ encoded_size: 22,
+ encryption_algorithm: 'none',
+ relative_url:
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ },
+ 'endpoint-exceptionlist-macos-v1': {
+ compression_algorithm: 'zlib',
+ decoded_sha256:
+ 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ decoded_size: 14,
+ encoded_sha256:
+ 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda',
+ encoded_size: 22,
+ encryption_algorithm: 'none',
+ relative_url:
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ },
+ 'endpoint-exceptionlist-windows-v1': {
+ compression_algorithm: 'zlib',
+ decoded_sha256:
+ 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ decoded_size: 14,
+ encoded_sha256:
+ 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda',
+ encoded_size: 22,
+ encryption_algorithm: 'none',
+ relative_url:
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ },
+ },
+ manifest_version: 'WzEwNSwxXQ==',
+ schema_version: 'v1',
+ },
policy: {
linux: {
events: { file: false, network: true, process: true },
diff --git a/x-pack/test_utils/testbed/testbed.ts b/x-pack/test_utils/testbed/testbed.ts
index febd2a63cf5b8..e981d0b6918a3 100644
--- a/x-pack/test_utils/testbed/testbed.ts
+++ b/x-pack/test_utils/testbed/testbed.ts
@@ -244,8 +244,7 @@ export const registerTestBed = (
const formInput = findTestSubject(comboBox, 'comboBoxSearchInput');
setInputValue(formInput, value);
- // keyCode 13 === ENTER
- comboBox.simulate('keydown', { keyCode: 13 });
+ comboBox.simulate('keydown', { key: 'Enter' });
component.update();
};
diff --git a/yarn.lock b/yarn.lock
index 2d575634686a3..390b89ea5ce7d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2230,10 +2230,10 @@
tabbable "^1.1.0"
uuid "^3.1.0"
-"@elastic/eui@24.1.0":
- version "24.1.0"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-24.1.0.tgz#40593cc474237e8c464d182faa50c748b3f66822"
- integrity sha512-Y7s327h0Z8dsO6MY7Sn1k5pOrf9ZjWH/ZE2gVtfBn2He5aFahS/+A434EBqFG0YV5W1VZtYiXtSj0AE1gjtrrw==
+"@elastic/eui@26.3.1":
+ version "26.3.1"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-26.3.1.tgz#c05cac2d0b246adb2aab7df54f34ecc6ad4f48e1"
+ integrity sha512-bNCMJIqdx7TrhXtbnYN6nxR4fjfTmdpB1ptVDThpt8+1rMSL7ZvxcVL4xg9Fjf6sKLcEQyXfDhr3u4+N3Sa4FA==
dependencies:
"@types/chroma-js" "^2.0.0"
"@types/enzyme" "^3.1.13"
@@ -2258,6 +2258,7 @@
react-virtualized "^9.21.2"
resize-observer-polyfill "^1.5.0"
tabbable "^3.0.0"
+ text-diff "^1.0.1"
uuid "^3.1.0"
"@elastic/filesaver@1.1.2":
@@ -29949,6 +29950,11 @@ test-exclude@^6.0.0:
glob "^7.1.4"
minimatch "^3.0.4"
+text-diff@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/text-diff/-/text-diff-1.0.1.tgz#6c105905435e337857375c9d2f6ca63e453ff565"
+ integrity sha1-bBBZBUNeM3hXN1ydL2ymPkU/9WU=
+
text-hex@1.0.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"