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/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/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 d04d4100e8632..4fc259d270e94 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, @@ -68,6 +73,6 @@ OptimizerConfig { exports[`prepares assets for distribution: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -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/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/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/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 7ff1d87f1bc55..3bd3dc61c044f 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -21,7 +21,7 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t import path from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -// @ts-ignore +// @ts-expect-error import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); @@ -36,6 +36,7 @@ const defaultIntegrateOptions = { sourceFileName: localePath, dryRun: false, ignoreIncompatible: false, + ignoreMalformed: false, ignoreMissing: false, ignoreUnused: false, config: { diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index d8ccccca15559..f9cd6dd1971c7 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -31,7 +31,8 @@ import { normalizePath, readFileAsync, writeFileAsync, - // @ts-ignore + verifyICUMessage, + // @ts-expect-error } from './utils'; import { I18nConfig } from './config'; @@ -41,6 +42,7 @@ export interface IntegrateOptions { sourceFileName: string; targetFileName?: string; dryRun: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; @@ -105,6 +107,23 @@ export function verifyMessages( } } + for (const messageId of localizedMessagesIds) { + const defaultMessage = defaultMessagesMap.get(messageId); + if (defaultMessage) { + try { + const message = localizedMessagesMap.get(messageId)!; + verifyICUMessage(message); + } catch (err) { + if (options.ignoreMalformed) { + localizedMessagesMap.delete(messageId); + options.log.warning(`Malformed translation ignored (${messageId}): ${err}`); + } else { + errorMessage += `\nMalformed translation (${messageId}): ${err}\n`; + } + } + } + } + if (errorMessage) { throw createFailError(errorMessage); } diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts index 5900bf5aff252..afaf3cd875a8a 100644 --- a/src/dev/i18n/tasks/check_compatibility.ts +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -22,13 +22,14 @@ import { integrateLocaleFiles, I18nConfig } from '..'; export interface I18nFlags { fix: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; } export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) { - const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags; + const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags; return config.translations.map((translationsPath) => ({ task: async ({ messages }: { messages: Map }) => { // If `fix` is set we should try apply all possible fixes and override translations file. @@ -37,6 +38,7 @@ export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: To ignoreIncompatible: fix || ignoreIncompatible, ignoreUnused: fix || ignoreUnused, ignoreMissing: fix || ignoreMissing, + ignoreMalformed: fix || ignoreMalformed, sourceFileName: translationsPath, targetFileName: fix ? translationsPath : undefined, config, diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index d5ef2de6a83ab..aaebc59d3de50 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -208,6 +208,28 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI } } +/** + * Verifies valid ICU message. + * @param message ICU message. + * @param messageId ICU message id + * @returns {undefined} + */ +export function verifyICUMessage(message) { + try { + parser.parse(message); + } catch (error) { + if (error.name === 'SyntaxError') { + const errorWithContext = createParserErrorMessage(message, { + loc: { + line: error.location.start.line, + column: error.location.start.column - 1, + }, + message: error.message, + }); + throw errorWithContext; + } + } +} /** * Extracts value references from the ICU message. * @param message ICU message. diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 97ea988b1de3a..70eeedac2b8b6 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -36,6 +36,7 @@ run( async ({ flags: { 'ignore-incompatible': ignoreIncompatible, + 'ignore-malformed': ignoreMalformed, 'ignore-missing': ignoreMissing, 'ignore-unused': ignoreUnused, 'include-config': includeConfig, @@ -48,12 +49,13 @@ run( fix && (ignoreIncompatible !== undefined || ignoreUnused !== undefined || + ignoreMalformed !== undefined || ignoreMissing !== undefined) ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} none of the --ignore-incompatible, --ignore-unused or --ignore-missing is allowed when --fix is set.` + )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.` ); } @@ -99,6 +101,7 @@ run( checkCompatibility( config, { + ignoreMalformed: !!ignoreMalformed, ignoreIncompatible: !!ignoreIncompatible, ignoreUnused: !!ignoreUnused, ignoreMissing: !!ignoreMissing, diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 23d66fae9f26e..25c3ea32783aa 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -31,6 +31,7 @@ run( 'ignore-incompatible': ignoreIncompatible = false, 'ignore-missing': ignoreMissing = false, 'ignore-unused': ignoreUnused = false, + 'ignore-malformed': ignoreMalformed = false, 'include-config': includeConfig, path, source, @@ -66,12 +67,13 @@ run( typeof ignoreIncompatible !== 'boolean' || typeof ignoreUnused !== 'boolean' || typeof ignoreMissing !== 'boolean' || + typeof ignoreMalformed !== 'boolean' || typeof dryRun !== 'boolean' ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} --ignore-incompatible, --ignore-unused, --ignore-missing, and --dry-run can't have values` + )} --ignore-incompatible, --ignore-unused, --ignore-malformed, --ignore-missing, and --dry-run can't have values` ); } @@ -97,6 +99,7 @@ run( ignoreIncompatible, ignoreUnused, ignoreMissing, + ignoreMalformed, config, log, }); 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/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/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/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/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 74119ca158685..00c5136d28110 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -7,5 +7,9 @@ "data", "inspector", "uiActions" + ], + "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 99% 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 6048a602b2bbd..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,10 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useEffect, useRef } from 'react'; import * as ReactDOM from 'react-dom'; 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 // in this case diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index 7ac05175c07e2..4440e1cbe1f62 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -10,5 +10,8 @@ "static/forms/hook_form_lib", "static/validators/string", "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/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..a55c2a61e7f1e 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", "share"] } 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/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/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 c0c0c65afd580..66b780ad68c11 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -7,5 +7,9 @@ "telemetryCollectionManager", "usageCollection" ], - "extraPublicDirs": ["common/constants"] + "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 44ecbbfa68408..5bd0400733cc6 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -2,5 +2,8 @@ "id": "uiActions", "version": "kibana", "server": false, - "ui": true + "ui": true, + "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_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_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_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/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/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/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/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/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 039f48ca1b58c..0960e12ed99e9 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 398241bb7fc95..def6f1f4346ff 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/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/plugins/apm/common/utils/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts index 08062cbf76bc6..9ffec18d95fb0 100644 --- a/x-pack/plugins/apm/common/utils/range_filter.ts +++ b/x-pack/plugins/apm/common/utils/range_filter.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export function rangeFilter( - start: number, - end: number, - timestampField = '@timestamp' -) { +export function rangeFilter(start: number, end: number) { return { - [timestampField]: { + '@timestamp': { gte: start, lte: end, format: 'epoch_millis', diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index ceed7b0dd5eb4..700ea6a15d293 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -26,5 +26,10 @@ "xpack", "apm" ], - "extraPublicDirs": ["public/style/variables"] + "extraPublicDirs": ["public/style/variables"], + "requiredBundles": [ + "kibanaReact", + "kibanaUtils", + "observability" + ] } diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 1e6015a9589b0..2f41b9fedd1d1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -25,7 +25,7 @@ export function AgentConfigurations() { (callApmApi) => callApmApi({ pathname: '/api/apm/settings/agent-configuration' }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); useTrackPageview({ app: 'apm', path: 'agent_configuration' }); 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 81655bc46c336..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 @@ -10,9 +10,15 @@ import { i18n } from '@kbn/i18n'; import { EuiPanel } from '@elastic/eui'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/useFetcher'; import { LicensePrompt } from '../../../shared/LicensePrompt'; import { useLicense } from '../../../../hooks/useLicense'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = { + jobs: [], + hasLegacyJobs: false, +}; export const AnomalyDetection = () => { const license = useLicense(); @@ -20,17 +26,13 @@ export const AnomalyDetection = () => { const [viewAddEnvironments, setViewAddEnvironments] = useState(false); - const { refetch, data = [], status } = useFetcher( + const { refetch, data = DEFAULT_VALUE, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; - const hasFetchFailure = status === FETCH_STATUS.FAILURE; - if (!hasValidLicense) { return ( @@ -60,13 +62,13 @@ 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.', })} {viewAddEnvironments ? ( environment)} + currentEnvironments={data.jobs.map(({ environment }) => environment)} onCreateJobSuccess={() => { refetch(); setViewAddEnvironments(false); @@ -77,9 +79,9 @@ export const AnomalyDetection = () => { /> ) : ( { setViewAddEnvironments(true); }} 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 30b4805011f03..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 @@ -16,12 +16,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink'; import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { LegacyJobsCallout } from './legacy_jobs_callout'; const columns: Array> = [ { @@ -60,17 +62,22 @@ const columns: Array> = [ ]; interface Props { - isLoading: boolean; - hasFetchFailure: boolean; + status: FETCH_STATUS; onAddEnvironments: () => void; anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[]; + hasLegacyJobs: boolean; } export const JobsList = ({ - isLoading, - hasFetchFailure, + status, onAddEnvironments, anomalyDetectionJobsByEnv, + hasLegacyJobs, }: Props) => { + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + + const hasFetchFailure = status === FETCH_STATUS.FAILURE; + return ( @@ -91,7 +98,7 @@ export const JobsList = ({ {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', { - defaultMessage: 'Add environments', + defaultMessage: 'Create ML Job', } )} @@ -101,7 +108,7 @@ export const JobsList = ({ @@ -131,6 +138,8 @@ export const JobsList = ({ items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv} /> + + {hasLegacyJobs && } ); }; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx new file mode 100644 index 0000000000000..54053097ab02e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx @@ -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 { EuiCallOut, EuiButton } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; + +export function LegacyJobsCallout() { + const { core } = useApmPluginContext(); + return ( + +

+ {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.body', + { + defaultMessage: + 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', + } + )} +

+ + {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', + { defaultMessage: 'Review jobs' } + )} + +
+ ); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts new file mode 100644 index 0000000000000..bfc4fcde09972 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction'; +export const APM_ML_JOB_GROUP = 'apm'; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 406097805775d..e74a546beb2d3 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -14,9 +14,7 @@ import { PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; - -const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction'; -export const ML_GROUP_NAME_APM = 'apm'; +import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -83,8 +81,8 @@ async function createAnomalyDetectionJob({ return ml.modules.setup({ moduleId: ML_MODULE_ID_APM_TRANSACTION, - prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`, - groups: [ML_GROUP_NAME_APM, convertedEnvironmentName], + prefix: `${APM_ML_JOB_GROUP}-${convertedEnvironmentName}-${randomToken}-`, + groups: [APM_ML_JOB_GROUP, convertedEnvironmentName], indexPatternName, query: { bool: { diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 252c87e9263db..0d00adbfedf41 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,56 +5,34 @@ */ import { Logger } from 'kibana/server'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; -import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection'; -import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs'; +import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group'; -export type AnomalyDetectionJobsAPIResponse = PromiseReturnType< - typeof getAnomalyDetectionJobs ->; -export async function getAnomalyDetectionJobs( - setup: Setup, - logger: Logger -): Promise { +export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; if (!ml) { return []; } - try { - const mlCapabilities = await ml.mlSystem.mlCapabilities(); - if ( - !( - mlCapabilities.mlFeatureEnabledInSpace && - mlCapabilities.isPlatinumOrTrialLicense - ) - ) { - logger.warn( - 'Anomaly detection integration is not availble for this user.' - ); - return []; - } - } catch (error) { - logger.warn('Unable to get ML capabilities.'); - logger.error(error); - return []; - } - try { - const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); - return jobs - .map((job) => { - const environment = job.custom_settings?.job_tags?.environment ?? ''; - return { - job_id: job.job_id, - environment, - }; - }) - .filter((job) => job.environment); - } catch (error) { - if (error.statusCode !== 404) { - logger.warn('Unable to get APM ML jobs.'); - logger.error(error); - } + + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if ( + !( + mlCapabilities.mlFeatureEnabledInSpace && + mlCapabilities.isPlatinumOrTrialLicense + ) + ) { + logger.warn('Anomaly detection integration is not availble for this user.'); return []; } + + const response = await getMlJobsWithAPMGroup(ml); + return response.jobs + .map((job) => { + const environment = job.custom_settings?.job_tags?.environment ?? ''; + return { + job_id: job.job_id, + environment, + }; + }) + .filter((job) => job.environment); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts new file mode 100644 index 0000000000000..5c0a3d17648aa --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.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 { Setup } from '../helpers/setup_request'; +import { APM_ML_JOB_GROUP } from './constants'; + +// returns ml jobs containing "apm" group +// workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned +export async function getMlJobsWithAPMGroup(ml: NonNullable) { + try { + return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + } catch (e) { + if (e.statusCode === 404) { + return { count: 0, jobs: [] }; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts new file mode 100644 index 0000000000000..bf502607fcc1d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.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 { Setup } from '../helpers/setup_request'; +import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group'; + +// Determine whether there are any legacy ml jobs. +// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction +export async function hasLegacyJobs(setup: Setup) { + const { ml } = setup; + + if (!ml) { + return false; + } + + const response = await getMlJobsWithAPMGroup(ml); + return response.jobs.some( + (job) => + job.job_id.endsWith('high_mean_response_time') && + job.custom_settings?.created_by === 'ml-module-apm-transaction' + ); +} diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 67eca0da946d0..7009470e1ff17 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -10,6 +10,7 @@ import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; +import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -17,7 +18,11 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ path: '/api/apm/settings/anomaly-detection', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await getAnomalyDetectionJobs(setup, context.logger); + const jobs = await getAnomalyDetectionJobs(setup, context.logger); + return { + jobs, + hasLegacyJobs: await hasLegacyJobs(setup), + }; }, })); 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/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/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/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/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/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_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 9f1e57b6e2b20..16da98796c08c 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/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/kibana.json b/x-pack/plugins/license_management/kibana.json index 6da923c5cff5a..3dbf99fced0b0 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -6,5 +6,9 @@ "requiredPlugins": ["home", "licensing", "management"], "optionalPlugins": ["telemetry"], "configPath": ["xpack", "license_management"], - "extraPublicDirs": ["common/constants"] + "extraPublicDirs": ["common/constants"], + "requiredBundles": [ + "telemetryManagementSection", + "kibanaReact" + ] } diff --git a/x-pack/plugins/licensing/kibana.json b/x-pack/plugins/licensing/kibana.json index 9edaa726c6ba9..2d38a82271eb0 100644 --- a/x-pack/plugins/licensing/kibana.json +++ b/x-pack/plugins/licensing/kibana.json @@ -4,5 +4,6 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "licensing"], "server": true, - "ui": true + "ui": true, + "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index 6cb88b19483ce..af29b3aa53ded 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -10,6 +10,7 @@ export const LIST_URL = '/api/lists'; export const LIST_INDEX = `${LIST_URL}/index`; export const LIST_ITEM_URL = `${LIST_URL}/items`; +export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`; /** * Exception list routes diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index b4f2639f24923..118bb2f927a64 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -48,7 +48,7 @@ export class ListPlugin core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); const router = core.http.createRouter(); - initRoutes(router, config); + initRoutes(router, config, plugins.security); return { getExceptionListClient: (savedObjectsClient, user): ExceptionListClient => { 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/maps/kibana.json b/x-pack/plugins/maps/kibana.json index f8a30b8d0337e..e422efb31cb0d 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -19,5 +19,11 @@ ], "ui": true, "server": true, - "extraPublicDirs": ["common/constants"] + "extraPublicDirs": ["common/constants"], + "requiredBundles": [ + "charts", + "kibanaReact", + "kibanaUtils", + "savedObjects" + ] } diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts index 751413bb6485a..d5c532234fd2b 100644 --- a/x-pack/plugins/ml/common/constants/new_job.ts +++ b/x-pack/plugins/ml/common/constants/new_job.ts @@ -17,6 +17,7 @@ export enum CREATED_BY_LABEL { MULTI_METRIC = 'multi-metric-wizard', POPULATION = 'population-wizard', CATEGORIZATION = 'categorization-wizard', + APM_TRANSACTION = 'ml-module-apm-transaction', } export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB'; 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/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/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/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/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/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_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..29d0ab58e8b55 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -11,7 +11,6 @@ "embeddable", "features", "home", - "ingestManager", "taskManager", "inspector", "licensing", @@ -21,6 +20,7 @@ ], "optionalPlugins": [ "encryptedSavedObjects", + "ingestManager", "ml", "newsfeed", "security", @@ -29,5 +29,11 @@ "lists" ], "server": true, - "ui": true + "ui": true, + "requiredBundles": [ + "kibanaUtils", + "esUiShared", + "kibanaReact", + "ingestManager" + ] } 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/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 e95ea4531d9ad..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 { @@ -162,7 +163,7 @@ export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, columns: alertsHeaders, showCheckboxes: true, - showRowRenderers: false, + excludedRowRendererIds: Object.values(RowRendererId), }; export const requiredFieldsForActions = [ 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 7bb4be6b50879..62328bd767748 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -324,10 +324,12 @@ export class Plugin implements IPlugin { 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/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ddb21c9aa12c2..d49494ada1767 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/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index d0a862d9ed882..b0e04427cddc2 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 63de117aeaf3d..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'; @@ -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/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 4274f278b64fa..9e47a81ae1d95 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/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index e212289458ed1..f9c773a2fa1ab 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -32,7 +32,7 @@ export interface StartPlugins { data: DataPublicPluginStart; embeddable: EmbeddableStart; inspector: InspectorStart; - ingestManager: IngestManagerStart; + ingestManager?: IngestManagerStart; newsfeed?: NewsfeedStart; triggers_actions_ui: TriggersActionsStart; uiActions: UiActionsStart; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts index 7642db23812e1..7b435f71fe4a8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts @@ -8,14 +8,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks'; import { EndpointAppContextService } from './endpoint_app_context_services'; describe('test endpoint app context services', () => { - // it('should return undefined on getAgentService if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getAgentService()).toEqual(undefined); - // }); - // it('should return undefined on getManifestManager if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getManifestManager()).toEqual(undefined); - // }); + it('should return undefined on getAgentService if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getAgentService()).toEqual(undefined); + }); + it('should return undefined on getManifestManager if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getManifestManager()).toEqual(undefined); + }); it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { const endpointAppContextService = new EndpointAppContextService(); expect(() => 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/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/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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b6e29f66ead2b..d5d7b1648e47f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -87,7 +87,6 @@ "advancedSettings.categoryNames.notificationsLabel": "通知", "advancedSettings.categoryNames.reportingLabel": "レポート", "advancedSettings.categoryNames.searchLabel": "検索", - "advancedSettings.categoryNames.securitySolutionLabel": "Security Solution", "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可視化", "advancedSettings.categorySearchLabel": "カテゴリー", @@ -124,6 +123,119 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", + "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", + "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", + "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", + "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", + "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス", + "apmOss.tutorial.apmAgents.title": "APM エージェント", + "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", + "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", + "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", + "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", + "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス", + "apmOss.tutorial.apmServer.title": "APM Server", + "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", + "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", + "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成", + "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、(例: 非 ASP.NET Core アプリケーションの場合)、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", + "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例: 「appsettings.json」ファイル) から構成を読み込みます。", + "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", + "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", + "apmOss.tutorial.dotNetClient.download.textPre": "[NuGet]({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Core の監視のみに [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) パッケージ、または Entity Framework Core の監視のみに [Elastic.Apm.EfCore]({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm]({elasticApmPackageLink}) パッケージを使用してください。", + "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", + "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", + "apmOss.tutorial.editConfig.title": "構成を編集する", + "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", + "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", + "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成", + "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", + "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", + "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行可能な名前が使用されます。", + "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", + "apmOss.tutorial.goClient.configure.title": "エージェントの構成", + "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", + "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", + "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", + "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", + "apmOss.tutorial.javaClient.download.textPre": "[Maven Central]({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", + "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加してエージェントをシステムプロパティで構成します。\n\n * 必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)\n * カスタム APM Server URL (デフォルト: {customApmServerUrl})\n * アプリケーションのベースパッケージを設定します", + "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APMサーバーのリアルユーザー監視サポートを有効にする", + "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)", + "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要)", + "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント]({docLink}) をご覧ください。", + "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", + "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", + "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加