diff --git a/api_docs/cases.json b/api_docs/cases.json index 3d8bbd1c9f2ba8..c5b74586e84423 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -4749,7 +4749,7 @@ "label": "ACTION_TYPES_URL", "description": [], "signature": [ - "\"/api/actions/list_action_types\"" + "\"/api/actions/connector_types\"" ], "source": { "path": "x-pack/plugins/cases/common/constants.ts", @@ -25001,4 +25001,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/kibana_react.json b/api_docs/kibana_react.json index 291cf94b448f42..d21f1bfbd9c175 100644 --- a/api_docs/kibana_react.json +++ b/api_docs/kibana_react.json @@ -3719,48 +3719,15 @@ "label": "onChange", "description": [], "signature": [ - "(value: string, event: ", + "((value: string, event: ", "editor", - ".IModelContentChangedEvent) => void" + ".IModelContentChangedEvent) => void) | undefined" ], "source": { "path": "src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx", "lineNumber": 27 }, - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "kibanaReact", - "id": "def-public.value", - "type": "string", - "tags": [], - "label": "value", - "description": [], - "source": { - "path": "src/plugins/kibana_react/public/code_editor/code_editor.tsx", - "lineNumber": 37 - }, - "deprecated": false - }, - { - "parentPluginId": "kibanaReact", - "id": "def-public.event", - "type": "Object", - "tags": [], - "label": "event", - "description": [], - "signature": [ - "editor", - ".IModelContentChangedEvent" - ], - "source": { - "path": "src/plugins/kibana_react/public/code_editor/code_editor.tsx", - "lineNumber": 37 - }, - "deprecated": false - } - ] + "deprecated": false }, { "parentPluginId": "kibanaReact", diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 9e94663cb1a48a..3a89efb7af28cd 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -18,7 +18,7 @@ import kibanaReactObj from './kibana_react.json'; | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 260 | 5 | 230 | 4 | +| 258 | 5 | 228 | 4 | ## Client diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md new file mode 100644 index 00000000000000..528be67f029c64 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) > [isSingleNamespaceType](./kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md) + +## SavedObjectMigrationContext.isSingleNamespaceType property + +Whether this is a single-namespace type or not + +Signature: + +```typescript +readonly isSingleNamespaceType: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md index c8a291e5028453..21ca234fde185c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md @@ -17,6 +17,7 @@ export interface SavedObjectMigrationContext | Property | Type | Description | | --- | --- | --- | | [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md) | string | The version in which this object type is being converted to a multi-namespace type | +| [isSingleNamespaceType](./kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md) | boolean | Whether this is a single-namespace type or not | | [log](./kibana-plugin-core-server.savedobjectmigrationcontext.log.md) | SavedObjectsMigrationLogger | logger instance to be used by the migration handler | | [migrationVersion](./kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md) | string | The migration version that this migration function is defined for | diff --git a/package.json b/package.json index 6c46361ff76fbb..2ed662e3660d3f 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "33.0.0", + "@elastic/charts": "33.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", diff --git a/packages/kbn-apm-config-loader/.babelrc b/packages/kbn-apm-config-loader/.babelrc new file mode 100644 index 00000000000000..7da72d17791281 --- /dev/null +++ b/packages/kbn-apm-config-loader/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index 09a121d71575c2..2ace393bcc4a30 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-apm-config-loader" PKG_REQUIRE_NAME = "@kbn/apm-config-loader" @@ -25,7 +26,7 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/elastic-safer-lodash-set", "//packages/kbn-utils", "@npm//js-yaml", @@ -33,13 +34,19 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/elastic-safer-lodash-set", + "//packages/kbn-utils", "@npm//@types/jest", "@npm//@types/js-yaml", "@npm//@types/lodash", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -50,14 +57,15 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - incremental = True, - out_dir = "target", + emit_declaration_only = True, + incremental = False, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -66,7 +74,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-apm-config-loader/package.json b/packages/kbn-apm-config-loader/package.json index c096ed2efb92a1..cdd4c62b9b8d41 100644 --- a/packages/kbn-apm-config-loader/package.json +++ b/packages/kbn-apm-config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@kbn/apm-config-loader", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index aa34b050616001..314c7969b3f949 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -1,14 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "stripInternal": false, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-apm-config-loader/src", + "stripInternal": false, "types": [ "jest", "node" diff --git a/packages/kbn-common-utils/.babelrc b/packages/kbn-common-utils/.babelrc new file mode 100644 index 00000000000000..7da72d17791281 --- /dev/null +++ b/packages/kbn-common-utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-common-utils/BUILD.bazel b/packages/kbn-common-utils/BUILD.bazel index 02446849733537..699f65da408f59 100644 --- a/packages/kbn-common-utils/BUILD.bazel +++ b/packages/kbn-common-utils/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-common-utils" PKG_REQUIRE_NAME = "@kbn/common-utils" @@ -23,18 +24,23 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-config-schema", - "@npm//load-json-file", "@npm//tslib", ] TYPES_DEPS = [ + "//packages/kbn-config-schema", + "@npm//tslib", "@npm//@types/jest", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -45,14 +51,15 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - incremental = True, - out_dir = "target", + emit_declaration_only = True, + incremental = False, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -61,7 +68,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-common-utils/package.json b/packages/kbn-common-utils/package.json index db99f4d6afb985..a9679c2f0fb180 100644 --- a/packages/kbn-common-utils/package.json +++ b/packages/kbn-common-utils/package.json @@ -1,8 +1,7 @@ { "name": "@kbn/common-utils", - "main": "./target/index.js", - "browser": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-common-utils/tsconfig.json b/packages/kbn-common-utils/tsconfig.json index 98f1b30c0d7ff2..7d1ecaa10a234e 100644 --- a/packages/kbn-common-utils/tsconfig.json +++ b/packages/kbn-common-utils/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": true, - "outDir": "target", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-common-utils/src", "types": [ diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b135f2d65c6fa3..5a0b376c4cbc5e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -101,7 +101,7 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 94301 spacesOss: 18817 - indexPatternFieldEditor: 90489 + indexPatternFieldEditor: 50000 osquery: 107090 fileUpload: 25664 dataVisualizer: 27530 diff --git a/packages/kbn-std/.babelrc b/packages/kbn-std/.babelrc new file mode 100644 index 00000000000000..7da72d17791281 --- /dev/null +++ b/packages/kbn-std/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel index e60577f0f08536..bcc5a87a1e795d 100644 --- a/packages/kbn-std/BUILD.bazel +++ b/packages/kbn-std/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-std" PKG_REQUIRE_NAME = "@kbn/std" @@ -23,7 +24,7 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-utility-types", "@npm//lodash", "@npm//query-string", @@ -32,12 +33,20 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-utility-types", + "@npm//query-string", + "@npm//rxjs", + "@npm//tslib", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -48,14 +57,15 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - incremental = True, - out_dir = "target", + emit_declaration_only = True, + incremental = False, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -64,7 +74,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index d88422ec1aa81b..cb4d6ad0d8aaf2 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -1,7 +1,7 @@ { "name": "@kbn/std", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index dec2d2df640861..3cff7fbd258a6d 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -1,14 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "stripInternal": true, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "./target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-std/src", + "stripInternal": true, "types": [ "jest", "node" diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 35b285d752611b..3372146894edbf 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -685,11 +685,13 @@ export const getEuiContextMapping = (): EuiTokensObject => { defaultMessage: 'Next page, {page}', values: { page }, }), - 'euiPagination.pageOfTotalCompressed': ({ page, total }: EuiValues) => - i18n.translate('core.euiPagination.pageOfTotalCompressed', { - defaultMessage: '{page} of {total}', - values: { page, total }, - }), + 'euiPagination.pageOfTotalCompressed': ({ page, total }: EuiValues) => ( + + ), 'euiPagination.previousPage': ({ page }: EuiValues) => i18n.translate('core.euiPagination.previousPage', { defaultMessage: 'Previous page, {page}', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index de8adc23996fd2..23f5d075d72e37 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -667,6 +667,7 @@ function wrapWithTry( log: new MigrationLogger(log), migrationVersion: version, convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, + isSingleNamespaceType: type.namespaceType === 'single', }); return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts index 4a62fcc95997bb..ef806aa5f0fca1 100644 --- a/src/core/server/saved_objects/migrations/mocks.ts +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -24,14 +24,17 @@ export const createSavedObjectsMigrationLoggerMock = (): jest.Mocked => { const mock = { log: createSavedObjectsMigrationLoggerMock(), migrationVersion, convertToMultiNamespaceTypeVersion, + isSingleNamespaceType, }; return mock; }; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 570315e780ebe8..fe5a79dac12c36 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -65,6 +65,10 @@ export interface SavedObjectMigrationContext { * The version in which this object type is being converted to a multi-namespace type */ readonly convertToMultiNamespaceTypeVersion?: string; + /** + * Whether this is a single-namespace type or not + */ + readonly isSingleNamespaceType: boolean; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 47782c2f98d3a9..fbc51acfdc8ef6 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2268,6 +2268,7 @@ export interface SavedObjectExportBaseOptions { // @public export interface SavedObjectMigrationContext { readonly convertToMultiNamespaceTypeVersion?: string; + readonly isSingleNamespaceType: boolean; readonly log: SavedObjectsMigrationLogger; readonly migrationVersion: string; } diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 68b133f382c355..e524d78a53e80a 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -5,5 +5,9 @@ "ui": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "usageCollection"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json index 4510a1ea7d0653..799173fed094f2 100644 --- a/src/plugins/charts/kibana.json +++ b/src/plugins/charts/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["expressions"] + "requiredPlugins": ["expressions"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx index e5ab8bf4d1a0d1..cab4eae41afb8b 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx @@ -56,7 +56,6 @@ export const JsonCodeEditorCommon = ({ languageId={XJsonLang.ID} width={width} value={jsonValue || ''} - onChange={() => {}} editorDidMount={onEditorDidMount} aria-label={codeEditorAriaLabel} options={{ diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index 68786871825ac3..5f6dabc26659fc 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -546,7 +546,6 @@ exports[`Source Viewer component renders json code editor 1`] = ` aria-label="Read only JSON view of an elasticsearch document" editorDidMount={[Function]} languageId="xjson" - onChange={[Function]} options={ Object { "automaticLayout": true, diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index 3fdb6bd27e0bac..a094ce39d6caa3 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -69,6 +69,10 @@ export interface DatatableColumnMeta { * index/table this column is based on */ index?: string; + /** + * names the domain this column represents + */ + dimensionName?: string; /** * serialized field format */ diff --git a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx index f7635f55aaee49..ec606b19c77d11 100644 --- a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -22,7 +22,7 @@ import { import { RangeControlEditor } from './range_control_editor'; import { ListControlEditor } from './list_control_editor'; import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern } from '../../../../data/public'; +import { IndexPattern } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; import './control_editor.scss'; @@ -35,7 +35,7 @@ interface ControlEditorUiProps { handleRemoveControl: (controlIndex: number) => void; handleIndexPatternChange: (controlIndex: number, indexPatternId: string) => void; handleFieldNameChange: (controlIndex: number, fieldName: string) => void; - getIndexPattern: (indexPatternId: string) => Promise; + getIndexPattern: (indexPatternId: string) => Promise; handleOptionsChange: ( controlIndex: number, optionName: T, diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx index 8afd2f198d2970..c6921b48e60203 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { ControlEditor } from './control_editor'; import { addControl, @@ -49,7 +49,7 @@ class ControlsTab extends PureComponent { type: CONTROL_TYPES.LIST, }; - getIndexPattern = async (indexPatternId: string): Promise => { + getIndexPattern = async (indexPatternId: string): Promise => { const [, startDeps] = await this.props.deps.core.getStartServices(); return await startDeps.data.indexPatterns.get(indexPatternId); }; diff --git a/src/plugins/input_control_vis/public/components/editor/field_select.tsx b/src/plugins/input_control_vis/public/components/editor/field_select.tsx index 0815b3bbe08ae1..5d3f2ef81bf085 100644 --- a/src/plugins/input_control_vis/public/components/editor/field_select.tsx +++ b/src/plugins/input_control_vis/public/components/editor/field_select.tsx @@ -12,7 +12,7 @@ import React, { Component } from 'react'; import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { IIndexPattern, IFieldType } from '../../../../data/public'; +import { IndexPattern, IFieldType } from '../../../../data/public'; interface FieldSelectUiState { isLoading: boolean; @@ -21,7 +21,7 @@ interface FieldSelectUiState { } export type FieldSelectUiProps = InjectedIntlProps & { - getIndexPattern: (indexPatternId: string) => Promise; + getIndexPattern: (indexPatternId: string) => Promise; indexPatternId: string; onChange: (value: any) => void; fieldName?: string; @@ -74,7 +74,7 @@ class FieldSelectUi extends Component { return; } - let indexPattern: IIndexPattern; + let indexPattern: IndexPattern; try { indexPattern = await this.props.getIndexPattern(indexPatternId); } catch (err) { diff --git a/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx index 61f7dd031d34b7..474238d01e4c89 100644 --- a/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -14,7 +14,7 @@ import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; +import { IndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface ListControlEditorState { @@ -25,7 +25,7 @@ interface ListControlEditorState { } interface ListControlEditorProps { - getIndexPattern: (indexPatternId: string) => Promise; + getIndexPattern: (indexPatternId: string) => Promise; controlIndex: number; controlParams: ControlParams; handleFieldNameChange: (fieldName: string) => void; @@ -104,7 +104,7 @@ export class ListControlEditor extends PureComponent< return; } - let indexPattern: IIndexPattern; + let indexPattern: IndexPattern; try { indexPattern = await this.props.getIndexPattern(this.props.controlParams.indexPattern); } catch (err) { diff --git a/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 0f1e61bde2813c..8be9e176cecc93 100644 --- a/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -14,13 +14,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; +import { IndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { controlIndex: number; controlParams: ControlParams; - getIndexPattern: (indexPatternId: string) => Promise; + getIndexPattern: (indexPatternId: string) => Promise; handleFieldNameChange: (fieldName: string) => void; handleIndexPatternChange: (indexPatternId: string) => void; handleOptionsChange: ( diff --git a/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts index 57bce49da763a0..122800198f092c 100644 --- a/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts +++ b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; /** * Returns forced **Partial** IndexPattern for use in tests */ -export const getIndexPatternMock = (): Promise => { +export const getIndexPatternMock = (): Promise => { return Promise.resolve({ id: 'mockIndexPattern', title: 'mockIndexPattern', @@ -20,5 +20,5 @@ export const getIndexPatternMock = (): Promise => { { name: 'textField', type: 'string', aggregatable: false }, { name: 'numberField', type: 'number', aggregatable: true }, ], - } as IIndexPattern); + } as IndexPattern); }; diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index 30aa32c6a2e894..8ef24e21ff578d 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -54,7 +54,6 @@ export const RequestCodeViewer = ({ json }: RequestCodeViewerProps) => ( {}} options={{ readOnly: true, lineNumbers: 'off', diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index e96b4859a36d0b..b9c808101b3914 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -2,5 +2,9 @@ "id": "kibanaLegacy", "version": "kibana", "server": true, - "ui": true + "ui": true, + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index d5e7bbb71cac88..6b02d8d159617c 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -34,7 +34,7 @@ export interface Props { value: string; /** Function invoked when text in editor is changed */ - onChange: (value: string, event: monaco.editor.IModelContentChangedEvent) => void; + onChange?: (value: string, event: monaco.editor.IModelContentChangedEvent) => void; /** * Options for the Monaco Code Editor diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 44c3f861709cee..ad1d6d01f1409d 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -4,5 +4,9 @@ "server": true, "ui": true, "optionalPlugins": ["home", "share"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index 3134cc265fba1f..b6513c46f80624 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -15,5 +15,9 @@ "visTypeTimelion", "savedObjects", "kibanaLegacy" - ] + ], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap index dc83d9fdf48ac5..6c43072b97c281 100644 --- a/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`interpreter/functions#pie logs correct datatable to inspector 1`] = ` +Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + }, + "name": "Count", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + }, + ], + "type": "datatable", +} +`; + exports[`interpreter/functions#pie returns an object with the correct structure 1`] = ` Object { "as": "pie_vis", diff --git a/src/plugins/vis_type_pie/public/pie_fn.test.ts b/src/plugins/vis_type_pie/public/pie_fn.test.ts index d387d4035e8ab5..3dcef406379c26 100644 --- a/src/plugins/vis_type_pie/public/pie_fn.test.ts +++ b/src/plugins/vis_type_pie/public/pie_fn.test.ts @@ -8,6 +8,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; +import { Datatable } from '../../expressions/common/expression_types/specs'; describe('interpreter/functions#pie', () => { const fn = functionWrapper(createPieVisFn()); @@ -50,4 +51,20 @@ describe('interpreter/functions#pie', () => { const actual = await fn(context, visConfig); expect(actual).toMatchSnapshot(); }); + + it('logs correct datatable to inspector', async () => { + let loggedTable: Datatable; + const handlers = { + inspectorAdapters: { + tables: { + logDatatable: (name: string, datatable: Datatable) => { + loggedTable = datatable; + }, + }, + }, + }; + await fn(context, visConfig, handlers as any); + + expect(loggedTable!).toMatchSnapshot(); + }); }); diff --git a/src/plugins/vis_type_pie/public/pie_fn.ts b/src/plugins/vis_type_pie/public/pie_fn.ts index 1b5b8574f93117..65ac648ca28684 100644 --- a/src/plugins/vis_type_pie/public/pie_fn.ts +++ b/src/plugins/vis_type_pie/public/pie_fn.ts @@ -7,8 +7,9 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/common'; import { PieVisParams, PieVisConfig } from './types'; +import { prepareLogTable } from '../../visualizations/public'; export const vislibPieName = 'pie_vis'; @@ -133,7 +134,33 @@ export const createPieVisFn = (): VisTypePieExpressionFunctionDefinition => ({ } as PieVisParams; if (handlers?.inspectorAdapters?.tables) { - handlers.inspectorAdapters.tables.logDatatable('default', context); + const logTable = prepareLogTable(context, [ + [ + [args.metric], + i18n.translate('visTypePie.function.dimension.metric', { + defaultMessage: 'Slice size', + }), + ], + [ + args.buckets, + i18n.translate('visTypePie.function.adimension.buckets', { + defaultMessage: 'Slice', + }), + ], + [ + args.splitColumn, + i18n.translate('visTypePie.function.dimension.splitcolumn', { + defaultMessage: 'Column split', + }), + ], + [ + args.splitRow, + i18n.translate('visTypePie.function.dimension.splitrow', { + defaultMessage: 'Row split', + }), + ], + ]); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); } return { diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx index 178854550aff16..135c23b21d7d44 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx @@ -74,7 +74,6 @@ export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => { {}} options={{ readOnly: true, lineNumbers: 'off', diff --git a/src/plugins/vis_type_xy/public/editor/common_config.tsx b/src/plugins/vis_type_xy/public/editor/common_config.tsx index 1815d9cfc429da..5cafbdd0a569cd 100644 --- a/src/plugins/vis_type_xy/public/editor/common_config.tsx +++ b/src/plugins/vis_type_xy/public/editor/common_config.tsx @@ -23,7 +23,13 @@ export function getOptionTabs(showElasticChartsOptions = false) { defaultMessage: 'Metrics & axes', }), editor: (props: VisEditorOptionsProps) => ( - + ), }, { diff --git a/src/plugins/vis_type_xy/public/editor/components/options/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/index.tsx index 7d29cbdc4fbdd7..a3e20dd22dd5a8 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/index.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/index.tsx @@ -23,6 +23,11 @@ export const PointSeriesOptions = ( > ) => ; -export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( - -); +export const MetricsAxisOptions = ( + props: ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } + > +) => ; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap index 4447fb2b64ef7d..9f79dc06463fe5 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap @@ -20,6 +20,7 @@ exports[`CustomExtentsOptions component should init with the default set of prop /> { expect(comp.find(YExtents).exists()).toBeFalsy(); }); + it('should hide YExtents when the switch is disabled', () => { + const comp = shallow(); + + expect(comp.find(YExtents).exists()).toBeFalsy(); + }); + it('should call setValueAxis when value is true', () => { const comp = shallow(); comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, true); diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx index 9efb92bbf3730f..2d3e819e960245 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx @@ -21,6 +21,7 @@ export interface CustomExtentsOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; setValueAxis(paramName: T, value: ValueAxis[T]): void; setValueAxisScale: SetScale; + disableAxisExtents?: boolean; } function CustomExtentsOptions({ @@ -28,6 +29,7 @@ function CustomExtentsOptions({ setMultipleValidity, setValueAxis, setValueAxisScale, + disableAxisExtents = false, }: CustomExtentsOptionsProps) { const invalidBoundsMarginMessage = i18n.translate( 'visTypeXy.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', @@ -111,9 +113,10 @@ function CustomExtentsOptions({ paramName="setYExtents" value={axisScale.setYExtents} setValue={onSetYExtentsChange} + disabled={disableAxisExtents} /> - {axisScale.setYExtents && ( + {axisScale.setYExtents && !disableAxisExtents && ( ({ describe('MetricsAxisOptions component', () => { let setValue: jest.Mock; - let defaultProps: ValidationVisOptionsProps; + let defaultProps: ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } + >; let axis: ValueAxis; let axisRight: ValueAxis; let chart: SeriesParam; @@ -81,6 +86,9 @@ describe('MetricsAxisOptions component', () => { defaultProps = { aggs: createAggs([aggCount]), isTabSelected: true, + extraProps: { + showElasticChartsOptions: false, + }, vis: { type: { type: ChartType.Area, @@ -236,7 +244,12 @@ describe('MetricsAxisOptions component', () => { const getProps = ( valuePosition1: Position = Position.Right, valuePosition2: Position = Position.Left - ): ValidationVisOptionsProps => ({ + ): ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } + > => ({ ...defaultProps, stateParams: { ...defaultProps.stateParams, @@ -374,7 +387,12 @@ describe('MetricsAxisOptions component', () => { describe('onCategoryAxisPositionChanged', () => { const getProps = ( position: Position = Position.Bottom - ): ValidationVisOptionsProps => ({ + ): ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } + > => ({ ...defaultProps, stateParams: { ...defaultProps.stateParams, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx index d25845f02e7a73..5454df3a165cde 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx @@ -43,8 +43,17 @@ export type ChangeValueAxis = ( const VALUE_AXIS_PREFIX = 'ValueAxis-'; -function MetricsAxisOptions(props: ValidationVisOptionsProps) { - const { stateParams, setValue, aggs, vis, isTabSelected } = props; +function MetricsAxisOptions( + props: ValidationVisOptionsProps< + VisParams, + { + // TODO: Remove when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + showElasticChartsOptions: boolean; + } + > +) { + const { stateParams, setValue, aggs, vis, isTabSelected, extraProps } = props; const setParamByIndex: SetParamByIndex = useCallback( (axesName, index, paramName, value) => { @@ -326,6 +335,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) { setMultipleValidity={props.setMultipleValidity} seriesParams={stateParams.seriesParams} valueAxes={stateParams.valueAxes} + isNewLibrary={extraProps?.showElasticChartsOptions} /> void; + isNewLibrary?: boolean; } function ValueAxesPanel(props: ValueAxesPanelProps) { @@ -149,6 +150,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { onValueAxisPositionChanged={props.onValueAxisPositionChanged} setParamByIndex={props.setParamByIndex} setMultipleValidity={props.setMultipleValidity} + isNewLibrary={props.isNewLibrary ?? false} /> diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index 1a38be83b9fc5e..d39bbf5bfa5329 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -36,6 +36,7 @@ export interface ValueAxisOptionsParams { setParamByIndex: SetParamByIndex; valueAxis: ValueAxis; setMultipleValidity: (paramName: string, isValid: boolean) => void; + isNewLibrary?: boolean; } export function ValueAxisOptions({ @@ -45,6 +46,7 @@ export function ValueAxisOptions({ onValueAxisPositionChanged, setParamByIndex, setMultipleValidity, + isNewLibrary = false, }: ValueAxisOptionsParams) { const setValueAxis = useCallback( (paramName: T, value: ValueAxis[T]) => @@ -191,6 +193,7 @@ export function ValueAxisOptions({ setMultipleValidity={setMultipleValidity} setValueAxisScale={setValueAxisScale} setValueAxis={setValueAxis} + disableAxisExtents={isNewLibrary && axis.scale.mode === 'percentage'} /> diff --git a/src/plugins/visualizations/common/expression_functions/index.ts b/src/plugins/visualizations/common/expression_functions/index.ts new file mode 100644 index 00000000000000..123fa28f183384 --- /dev/null +++ b/src/plugins/visualizations/common/expression_functions/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './range'; +export * from './vis_dimension'; diff --git a/src/plugins/visualizations/public/expression_functions/range.ts b/src/plugins/visualizations/common/expression_functions/range.ts similarity index 97% rename from src/plugins/visualizations/public/expression_functions/range.ts rename to src/plugins/visualizations/common/expression_functions/range.ts index c5408a399e235c..04751ef01985ee 100644 --- a/src/plugins/visualizations/public/expression_functions/range.ts +++ b/src/plugins/visualizations/common/expression_functions/range.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Datatable, Range } from '../../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Range } from '../../../expressions/common'; interface Arguments { from: number; diff --git a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts b/src/plugins/visualizations/common/expression_functions/vis_dimension.ts similarity index 98% rename from src/plugins/visualizations/public/expression_functions/vis_dimension.ts rename to src/plugins/visualizations/common/expression_functions/vis_dimension.ts index cc29ee6ea39871..6886fa94f878ea 100644 --- a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts +++ b/src/plugins/visualizations/common/expression_functions/vis_dimension.ts @@ -12,7 +12,7 @@ import { ExpressionValueBoxed, Datatable, DatatableColumn, -} from '../../../expressions/public'; +} from '../../../expressions/common'; interface Arguments { accessor: string | number; diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts index c93e0ac3cda5a0..bde292cb69697f 100644 --- a/src/plugins/visualizations/common/index.ts +++ b/src/plugins/visualizations/common/index.ts @@ -8,3 +8,5 @@ /** @public types */ export * from './types'; +export * from './prepare_log_table'; +export * from './expression_functions'; diff --git a/src/plugins/visualizations/common/prepare_log_table.test.ts b/src/plugins/visualizations/common/prepare_log_table.test.ts new file mode 100644 index 00000000000000..dc02adbd458ee1 --- /dev/null +++ b/src/plugins/visualizations/common/prepare_log_table.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { prepareLogTable } from './prepare_log_table'; + +describe('prepareLogTable', () => { + test('returns first matching dimension name', () => { + const datatable = { + columns: [ + { + meta: {}, + }, + { + meta: {}, + }, + { + meta: {}, + }, + ], + }; + const logTable = prepareLogTable(datatable as any, [ + [[{ accessor: 0 } as any], 'dimension1'], + [[{ accessor: 2 } as any], 'dimension3'], + [[{ accessor: 1 } as any], 'dimension2'], + ]); + expect(logTable).toMatchInlineSnapshot( + { + columns: [ + { + meta: { + dimensionName: 'dimension1', + }, + }, + { + meta: { + dimensionName: 'dimension2', + }, + }, + { + meta: { + dimensionName: 'dimension3', + }, + }, + ], + }, + ` + Object { + "columns": Array [ + Object { + "meta": Object { + "dimensionName": "dimension1", + }, + }, + Object { + "meta": Object { + "dimensionName": "dimension2", + }, + }, + Object { + "meta": Object { + "dimensionName": "dimension3", + }, + }, + ], + } + ` + ); + }); +}); diff --git a/src/plugins/visualizations/common/prepare_log_table.ts b/src/plugins/visualizations/common/prepare_log_table.ts new file mode 100644 index 00000000000000..f179eb8a1c10a9 --- /dev/null +++ b/src/plugins/visualizations/common/prepare_log_table.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionValueVisDimension } from './expression_functions/vis_dimension'; +import { Datatable } from '../../expressions/common/expression_types/specs'; + +export type Dimension = [ExpressionValueVisDimension[] | undefined, string]; + +const getDimensionName = (columnIndex: number, dimensions: Dimension[]) => { + for (const dimension of dimensions) { + if (dimension[0]?.find((d) => d.accessor === columnIndex)) { + return dimension[1]; + } + } +}; + +export const prepareLogTable = (datatable: Datatable, dimensions: Dimension[]) => { + return { + ...datatable, + columns: datatable.columns.map((column, columnIndex) => { + return { + ...column, + meta: { + ...column.meta, + dimensionName: getDimensionName(columnIndex, dimensions), + }, + }; + }), + }; +}; diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index aa78e45481aa4d..daf2c90c2eb797 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -13,7 +13,7 @@ ], "optionalPlugins": ["usageCollection"], "requiredBundles": ["kibanaUtils", "discover"], - "extraPublicDirs": ["common/constants"], + "extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 081023d0039e78..a01a2477dd311f 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -41,5 +41,5 @@ export { } from './types'; export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; -export { SavedVisState, VisParams } from '../common'; -export { ExpressionValueVisDimension } from './expression_functions/vis_dimension'; +export { SavedVisState, VisParams, prepareLogTable } from '../common'; +export { ExpressionValueVisDimension } from '../common/expression_functions/vis_dimension'; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 17dcb28e15e5dc..e3dcf79a63a768 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -31,8 +31,8 @@ import { createVisEmbeddableFromObject, } from './embeddable'; import { TypesService } from './vis_types/types_service'; -import { range as rangeExpressionFunction } from './expression_functions/range'; -import { visDimension as visDimensionExpressionFunction } from './expression_functions/vis_dimension'; +import { range as rangeExpressionFunction } from '../common/expression_functions/range'; +import { visDimension as visDimensionExpressionFunction } from '../common/expression_functions/vis_dimension'; import { createStartServicesGetter, StartServicesGetter } from '../../kibana_utils/public'; import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts index 0f8a5428c68856..b101a3c2feae96 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -47,6 +47,7 @@ describe('useVisualizeAppState', () => { const savedVisInstance = ({ vis: { setState: jest.fn().mockResolvedValue({}), + data: {}, }, savedVis: {}, embeddableHandler: {}, @@ -167,7 +168,31 @@ describe('useVisualizeAppState', () => { const { aggs, ...visState } = stateContainer.getState().vis; const expectedNewVisState = { ...visState, - data: { aggs: state.vis.aggs }, + data: { aggs: state.vis.aggs, searchSource: { query: state.query, filter: state.filters } }, + }; + + expect(savedVisInstance.vis.setState).toHaveBeenCalledWith(expectedNewVisState); + expect(result.current).toEqual({ + appState: stateContainer, + hasUnappliedChanges: false, + }); + }); + + it('should successfully updated vis state and set up app state container if query from app state is different', async () => { + stateContainerGetStateMock.mockImplementation(() => ({ + ...visualizeAppStateStub, + query: { query: 'test', language: 'kuery' }, + })); + const { result, waitForNextUpdate } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + await waitForNextUpdate(); + + const { aggs, ...visState } = stateContainer.getState().vis; + const expectedNewVisState = { + ...visState, + data: { aggs: state.vis.aggs, searchSource: { query: state.query, filter: state.filters } }, }; expect(savedVisInstance.vis.setState).toHaveBeenCalledWith(expectedNewVisState); diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index 43511258d23945..4dbf621c2e564a 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -44,6 +44,7 @@ export const useVisualizeAppState = ( kbnUrlStateStorage: services.kbnUrlStateStorage, byValue, }); + const currentAppState = stateContainer.getState(); const onDirtyStateChange = ({ isDirty }: { isDirty: boolean }) => { if (!isDirty) { @@ -57,8 +58,8 @@ export const useVisualizeAppState = ( const { filterManager, queryString } = services.data.query; // sync initial app state from state to managers - filterManager.setAppFilters(cloneDeep(stateContainer.getState().filters)); - queryString.setQuery(migrateLegacyQuery(stateContainer.getState().query)); + filterManager.setAppFilters(cloneDeep(currentAppState.filters)); + queryString.setQuery(migrateLegacyQuery(currentAppState.query)); // setup syncing of app filters between appState and query services const stopSyncingAppFilters = connectToQueryState( @@ -90,10 +91,20 @@ export const useVisualizeAppState = ( // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the // defaults applied. If the url was from a previous session which included modifications to the // appState then they won't be equal. - if (!isEqual(stateContainer.getState().vis, stateDefaults.vis)) { - const { aggs, ...visState } = stateContainer.getState().vis; + if ( + !isEqual(currentAppState.vis, stateDefaults.vis) || + !isEqual(currentAppState.query, stateDefaults.query) || + !isEqual(currentAppState.filters, stateDefaults.filters) + ) { + const { aggs, ...visState } = currentAppState.vis; + const query = currentAppState.query; + const filter = currentAppState.filters; + const visSearchSource = instance.vis.data.searchSource?.getFields() || {}; instance.vis - .setState({ ...visState, data: { aggs } }) + .setState({ + ...visState, + data: { aggs, searchSource: { ...visSearchSource, query, filter } }, + }) .then(() => { // setting up the stateContainer after setState is successful will prevent loading the editor with failures // otherwise the catch will take presedence diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/_visualize_listing.ts index 78474693e939ab..30b19b52af258e 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/_visualize_listing.ts @@ -12,8 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor']); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/40912 - describe.skip('visualize listing page', function describeIndexTests() { + describe('visualize listing page', function describeIndexTests() { const vizName = 'Visualize Listing Test'; describe('create and delete', function () { @@ -32,9 +31,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('delete all viz', async function () { await PageObjects.visualize.createSimpleMarkdownViz(vizName + '1'); - await PageObjects.visualize.createSimpleMarkdownViz(vizName + '2'); await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.expectItemsCount('visualize', 2); + await PageObjects.visualize.createSimpleMarkdownViz(vizName + '2'); + await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.expectItemsCount('visualize', 3); await PageObjects.visualize.deleteAllVisualizations(); diff --git a/x-pack/examples/embedded_lens_example/kibana.json b/x-pack/examples/embedded_lens_example/kibana.json index be38e5bf3e7612..13417c658f8750 100644 --- a/x-pack/examples/embedded_lens_example/kibana.json +++ b/x-pack/examples/embedded_lens_example/kibana.json @@ -12,5 +12,9 @@ "developerExamples" ], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": [], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index e1e74e35a7bb2d..3a4ac4a9e5daef 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -49,7 +49,7 @@ function wrapper({ children }: { children?: ReactNode }) { }) as unknown) as ApmPluginContextValue; return ( - + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 793ca6f0655dfc..1e945aca7c916f 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -28,12 +28,12 @@ import { asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; +import { useApmParams } from '../../../../hooks/use_apm_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { truncate, unit } from '../../../../utils/style'; -import { AgentIcon } from '../../../shared/agent_icon'; +import { unit } from '../../../../utils/style'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; -import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview_link'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; +import { ServiceLink } from '../../../shared/service_link'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; @@ -50,19 +50,10 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const AppLink = euiStyled(ServiceOrTransactionsOverviewLink)` - font-size: ${({ theme }) => theme.eui.euiFontSizeM} - ${truncate('100%')}; -`; - const ToolTipWrapper = euiStyled.span` width: 100%; .apmServiceList__serviceNameTooltip { width: 100%; - .apmServiceList__serviceNameContainer { - // removes 24px referent to the icon placed on the left side of the text. - width: calc(100% - 24px); - } } `; @@ -74,8 +65,10 @@ const SERVICE_HEALTH_STATUS_ORDER = [ ]; export function getServiceColumns({ + query, showTransactionTypeColumn, }: { + query: Record; showTransactionTypeColumn: boolean; }): Array> { return [ @@ -101,31 +94,19 @@ export function getServiceColumns({ }), width: '40%', sortable: true, - render: (_, { serviceName, agentName, transactionType }) => ( - + render: (_, { serviceName, agentName }) => ( + - - {agentName && ( - - - - )} - - - {formatString(serviceName)} - - - + ), @@ -223,9 +204,11 @@ export function ServiceList({ items, noItemsMessage }: Props) { transactionType !== TRANSACTION_PAGE_LOAD ); + const { query } = useApmParams('/services'); + const serviceColumns = useMemo( - () => getServiceColumns({ showTransactionTypeColumn }), - [showTransactionTypeColumn] + () => getServiceColumns({ query, showTransactionTypeColumn }), + [query, showTransactionTypeColumn] ); const columns = displayHealthStatus diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx index e09ddac525db02..c83f9995caf2e3 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx @@ -15,9 +15,9 @@ import { items } from './__fixtures__/service_api_mock_data'; function Wrapper({ children }: { children?: ReactNode }) { return ( - - {children} - + + {children} + ); } @@ -57,6 +57,7 @@ describe('ServiceList', () => { environments: ['test'], }; const renderedColumns = getServiceColumns({ + query: {}, showTransactionTypeColumn: false, }).map((c) => c.render!(service[c.field!], service)); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index f852db6c5cbcf2..fb68f1266ee9a9 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -15,9 +15,6 @@ import { import { i18n } from '@kbn/i18n'; import { keyBy } from 'lodash'; import React from 'react'; -import { EuiLink } from '@elastic/eui'; -import { useApmParams } from '../../../../hooks/use_apm_params'; -import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; import { asMillisecondDuration, @@ -29,14 +26,14 @@ import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/off import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies'; import { Coordinate } from '../../../../../typings/timeseries'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { unit } from '../../../../utils/style'; -import { AgentIcon } from '../../../shared/agent_icon'; +import { BackendLink } from '../../../shared/backend_link'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink'; -import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link'; -import { SpanIcon } from '../../../shared/span_icon'; +import { ServiceLink } from '../../../shared/service_link'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; @@ -100,9 +97,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { urlParams: { start, end, environment, comparisonEnabled, comparisonType }, } = useUrlParams(); - const { - query: { rangeFrom, rangeTo, kuery }, - } = useApmParams('/services/:serviceName/overview'); + const { query } = useApmParams('/services/:serviceName/overview'); const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ start, @@ -111,8 +106,6 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { comparisonType, }); - const apmRouter = useApmRouter(); - const columns: Array> = [ { field: 'name', @@ -127,37 +120,26 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { - - {item.type === 'service' ? ( - - ) : ( - - )} - - - {item.type === 'service' ? ( - - {item.name} - - ) : ( - - {item.name} - - )} - - + item.type === 'service' ? ( + + ) : ( + + ) } /> ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx index 0ad0fe872a8408..6bbcfcf545ee1c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx @@ -7,15 +7,16 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useUrlParams } from '../../../../../../context/url_params_context/use_url_params'; -import { getNextEnvironmentUrlParam } from '../../../../../../../common/environment_filter_values'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../../../../../common/elasticsearch_fieldnames'; +import { getNextEnvironmentUrlParam } from '../../../../../../../common/environment_filter_values'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview_link'; +import { useUrlParams } from '../../../../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../../../../hooks/use_apm_params'; import { TransactionDetailLink } from '../../../../../shared/Links/apm/transaction_detail_link'; +import { ServiceLink } from '../../../../../shared/service_link'; import { StickyProperties } from '../../../../../shared/sticky_properties'; interface Props { @@ -24,8 +25,9 @@ interface Props { export function FlyoutTopLevelProperties({ transaction }: Props) { const { - urlParams: { environment, latencyAggregationType }, + urlParams: { latencyAggregationType }, } = useUrlParams(); + const { query } = useApmParams('/services/:serviceName/transactions/view'); if (!transaction) { return null; @@ -33,7 +35,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { const nextEnvironment = getNextEnvironmentUrlParam({ requestedEnvironment: transaction.service.environment, - currentEnvironmentUrlParam: environment, + currentEnvironmentUrlParam: query.environment, }); const stickyProperties = [ @@ -43,12 +45,11 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { }), fieldName: SERVICE_NAME, val: ( - - {transaction.service.name} - + /> ), width: '25%', }, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx index d7613699221b48..d31af783e08c23 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx @@ -17,7 +17,7 @@ import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; import { getAgentMarks } from '../Marks/get_agent_marks'; import { getErrorMarks } from '../Marks/get_error_marks'; import { AccordionWaterfall } from './accordion_waterfall'; -import { WaterfallFlyout } from './WaterfallFlyout'; +import { WaterfallFlyout } from './waterfall_flyout'; import { IWaterfall, IWaterfallItem, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx index 55e726b9d6f0e7..93acf38a094bc0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx @@ -34,7 +34,7 @@ import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; import { ResponsiveFlyout } from '../ResponsiveFlyout'; import { SyncBadge } from '../sync_badge'; import { DatabaseContext } from './database_context'; -import { StickySpanProperties } from './StickySpanProperties'; +import { StickySpanProperties } from './sticky_span_properties'; function formatType(type: string) { switch (type) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx new file mode 100644 index 00000000000000..c736f9df214b90 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/span_flyout.stories.tsx @@ -0,0 +1,317 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentProps, ComponentType } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { SpanFlyout } from './'; + +type Args = ComponentProps; + +export default { + title: 'app/TransactionDetails/Waterfall/SpanFlyout', + component: SpanFlyout, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + + + + + + ); + }, + ], +}; + +export const TransactionSpan: Story = (args) => { + return ; +}; +TransactionSpan.args = { + totalDuration: 7589, + span: { + parent: { + id: '969fe48e33f4e13c', + }, + agent: { + name: 'ruby', + version: '4.2.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'apm-apm-server-59fb846665-mjb2l', + id: '1e6ecda2-1347-4800-b5d1-6d9eb3805d1d', + ephemeral_id: 'd810d578-a7fe-4c7d-8897-b502db141800', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '1d63e25e7345627176e172ae690f9462', + }, + '@timestamp': '2021-07-28T02:24:31.803Z', + service: { + environment: 'production', + name: 'opbeans-ruby', + }, + transaction: { + id: '969fe48e33f4e13c', + }, + timestamp: { + us: 1627439071803773, + }, + span: { + duration: { + us: 5613, + }, + subtype: 'controller', + name: 'Api::CustomersController#index', + action: 'action', + id: 'dda84b0e3632fbb1', + type: 'app', + }, + }, + parentTransaction: { + container: { + id: '399a87146c0036592f6ee78553324b10c00757e024143913c97993384751e15e', + }, + kubernetes: { + pod: { + uid: 'dab8ed46-bab6-427c-ba79-6518cc17b60e', + }, + }, + process: { + args: ['-C', 'config/puma.rb'], + pid: 41, + title: '/usr/local/bundle/bin/puma', + }, + agent: { + name: 'ruby', + version: '4.2.0', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + domain: '10.43.242.237', + full: 'http://10.43.242.237:3000/api/customers', + }, + labels: { + company: 'opbeans', + }, + cloud: { + availability_zone: 'europe-west1-c', + instance: { + name: 'gke-edge-oblt-gcp-edge-oblt-gcp-pool-b6b9e929-92m2', + id: '4295368814211072338', + }, + provider: 'gcp', + machine: { + type: 'projects/8560181848/machineTypes/n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'europe-west1', + }, + observer: { + hostname: 'apm-apm-server-59fb846665-k7grs', + id: 'fcd83d41-d48c-4c84-8aa0-6bb9d4fe374d', + type: 'apm-server', + ephemeral_id: 'e325639d-aac5-4b94-a02d-19836ccdc17f', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '1d63e25e7345627176e172ae690f9462', + }, + '@timestamp': '2021-07-28T02:24:31.802Z', + ecs: { + version: '1.10.0', + }, + service: { + node: { + name: + '399a87146c0036592f6ee78553324b10c00757e024143913c97993384751e15e', + }, + environment: 'production', + framework: { + name: 'Ruby on Rails', + version: '6.1.4', + }, + name: 'opbeans-ruby', + runtime: { + name: 'ruby', + version: '2.7.3', + }, + language: { + name: 'ruby', + version: '2.7.3', + }, + version: '2021-07-27 03:47:02', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.40.0.249', + architecture: 'x86_64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + Version: ['HTTP/1.1'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['10.43.242.237:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'GET', + 'body.original': '[SKIPPED]', + socket: { + encrypted: false, + }, + env: { + ORIGINAL_FULLPATH: '/api/customers', + GATEWAY_INTERFACE: 'CGI/1.2', + SERVER_PORT: '3000', + SERVER_PROTOCOL: 'HTTP/1.1', + REQUEST_URI: '/api/customers', + REMOTE_ADDR: '10.40.3.37', + ORIGINAL_SCRIPT_NAME: '', + SERVER_SOFTWARE: 'puma 5.3.2 Sweetnighter', + QUERY_STRING: '', + ROUTES_9220_SCRIPT_NAME: '', + SCRIPT_NAME: '', + REQUEST_METHOD: 'GET', + REQUEST_PATH: '/api/customers', + SERVER_NAME: '10.43.242.237', + PATH_INFO: '/api/customers', + }, + }, + response: { + headers: { + 'X-Frame-Options': ['SAMEORIGIN'], + 'Referrer-Policy': ['strict-origin-when-cross-origin'], + Etag: ['W/"5eb0bc6061d718b6394c1a21d1fbc1fd"'], + 'Cache-Control': ['max-age=0, private, must-revalidate'], + 'X-Request-Id': ['db52f681-e7c2-4f44-b995-e17aa8ba8346'], + 'X-Content-Type-Options': ['nosniff'], + 'X-Runtime': ['0.007023'], + 'X-Xss-Protection': ['1; mode=block'], + 'X-Download-Options': ['noopen'], + Vary: ['Accept'], + 'X-Permitted-Cross-Domain-Policies': ['none'], + 'Content-Type': ['application/json; charset=utf-8'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + user: { + id: '3229', + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Python aiohttp', + device: { + name: 'Other', + }, + version: '3.3.2', + }, + transaction: { + duration: { + us: 7589, + }, + result: 'HTTP 2xx', + name: 'Api::CustomersController#index', + span_count: { + dropped: 0, + started: 2, + }, + id: '969fe48e33f4e13c', + type: 'request', + sampled: true, + }, + timestamp: { + us: 1627439071802242, + }, + }, +}; + +export const DatabaseSpan: Story = (args) => { + return ; +}; +DatabaseSpan.args = { + totalDuration: 7589, + span: { + parent: { + id: 'dda84b0e3632fbb1', + }, + agent: { + name: 'ruby', + version: '4.2.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'apm-apm-server-59fb846665-mjb2l', + id: '1e6ecda2-1347-4800-b5d1-6d9eb3805d1d', + type: 'apm-server', + ephemeral_id: 'd810d578-a7fe-4c7d-8897-b502db141800', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '1d63e25e7345627176e172ae690f9462', + }, + '@timestamp': '2021-07-28T02:24:31.805Z', + service: { + environment: 'production', + name: 'opbeans-ruby', + }, + transaction: { + id: '969fe48e33f4e13c', + }, + span: { + duration: { + us: 1234, + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + }, + }, + name: 'SELECT FROM customers', + action: 'sql', + id: 'da4c078e1bc72004', + type: 'db', + db: { + statement: 'SELECT "customers".* FROM "customers"', + type: 'sql', + }, + }, + timestamp: { + us: 1627439071805402, + }, + }, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx similarity index 69% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx index 9e1174818c43be..af55b68f813a14 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx @@ -7,18 +7,20 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useUrlParams } from '../../../../../../../context/url_params_context/use_url_params'; -import { getNextEnvironmentUrlParam } from '../../../../../../../../common/environment_filter_values'; import { SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_NAME, TRANSACTION_NAME, } from '../../../../../../../../common/elasticsearch_fieldnames'; +import { getNextEnvironmentUrlParam } from '../../../../../../../../common/environment_filter_values'; import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview_link'; +import { useApmParams } from '../../../../../../../hooks/use_apm_params'; +import { BackendLink } from '../../../../../../shared/backend_link'; import { TransactionDetailLink } from '../../../../../../shared/Links/apm/transaction_detail_link'; +import { ServiceLink } from '../../../../../../shared/service_link'; import { StickyProperties } from '../../../../../../shared/sticky_properties'; interface Props { @@ -27,9 +29,8 @@ interface Props { } export function StickySpanProperties({ span, transaction }: Props) { - const { - urlParams: { environment, latencyAggregationType }, - } = useUrlParams(); + const { query } = useApmParams('/services/:serviceName/transactions/view'); + const { environment, latencyAggregationType } = query; const nextEnvironment = getNextEnvironmentUrlParam({ requestedEnvironment: transaction?.service.environment, @@ -37,6 +38,8 @@ export function StickySpanProperties({ span, transaction }: Props) { }); const spanName = span.span.name; + const backendName = span.span.destination?.service.resource; + const transactionStickyProperties = transaction ? [ { @@ -45,12 +48,11 @@ export function StickySpanProperties({ span, transaction }: Props) { }), fieldName: SERVICE_NAME, val: ( - - {transaction.service.name} - + /> ), width: '25%', }, @@ -80,6 +82,29 @@ export function StickySpanProperties({ span, transaction }: Props) { ] : []; + const backendStickyProperties = backendName + ? [ + { + label: i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.backendLabel', + { + defaultMessage: 'Backend', + } + ), + fieldName: SPAN_DESTINATION_SERVICE_RESOURCE, + val: ( + + ), + width: '25%', + }, + ] + : []; + const stickyProperties = [ { label: i18n.translate( @@ -89,10 +114,11 @@ export function StickySpanProperties({ span, transaction }: Props) { } ), fieldName: SPAN_NAME, - val: spanName || NOT_AVAILABLE_LABEL, + val: spanName ?? NOT_AVAILABLE_LABEL, truncated: true, width: '25%', }, + ...backendStickyProperties, ...transactionStickyProperties, ]; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.tsx new file mode 100644 index 00000000000000..7e2365ffb8aff9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/transaction_flyout.stories.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentProps, ComponentType } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { TransactionFlyout } from './'; + +type Args = ComponentProps; + +export default { + title: 'app/TransactionDetails/Waterfall/TransactionFlyout', + component: TransactionFlyout, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + + + + + + ); + }, + ], +}; + +export const Example: Story = (args) => { + return ; +}; +Example.args = { + transaction: { + container: { + id: '4810e1f4da909044f1f6f56be41a542dc59784948f059769d6a590952deca405', + }, + kubernetes: { + pod: { + uid: 'c44f58b9-d1fa-4341-a40a-4fdcac0ec4d0', + name: 'opbeans-java-6995b65cc9-gtwzd', + }, + }, + agent: { + name: 'java', + ephemeral_id: '12f7e44a-8b1d-467b-afe0-46b9a1c60535', + version: '1.25.1-SNAPSHOT.UNKNOWN', + }, + process: { + pid: 8, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + domain: '10.43.242.237', + full: 'http://10.43.242.237:3000/api/orders', + }, + cloud: { + availability_zone: 'europe-west1-c', + instance: { + name: 'gke-edge-oblt-gcp-edge-oblt-gcp-pool-b6b9e929-92m2', + id: '4295368814211072338', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'europe-west1', + }, + observer: { + hostname: 'apm-apm-server-57659d6b4c-zvspw', + id: 'ab52d330-8ad1-4eb2-b692-92b8fa2ddcce', + type: 'apm-server', + ephemeral_id: 'aeb9add4-1ee1-4194-bbea-989de999c44a', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: 'b80358b455b1075670cbc5fe57aa6d64', + }, + '@timestamp': '2021-07-28T17:34:04.335Z', + ecs: { + version: '1.10.0', + }, + service: { + node: { + name: + '4810e1f4da909044f1f6f56be41a542dc59784948f059769d6a590952deca405', + }, + environment: 'production', + framework: { + name: 'Servlet API', + }, + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.11', + }, + language: { + name: 'Java', + version: '11.0.11', + }, + version: '2021-07-28 03:47:59', + }, + host: { + os: { + platform: 'Linux', + }, + ip: '10.40.0.104', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['10.43.242.237:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'GET', + socket: { + encrypted: false, + remote_address: '10.40.2.215', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Wed, 28 Jul 2021 17:34:04 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + transaction: { + duration: { + us: 23961, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: 'ade1f180d840845c', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Python aiohttp', + device: { + name: 'Other', + }, + version: '3.3.2', + }, + timestamp: { + us: 1627493644335001, + }, + }, + rootTransactionDuration: 23961, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx index ec6d550affb91b..4163388db1ec00 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx @@ -9,7 +9,7 @@ import { History } from 'history'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { SpanFlyout } from './span_flyout'; -import { TransactionFlyout } from './TransactionFlyout'; +import { TransactionFlyout } from './transaction_flyout'; import { IWaterfall } from './waterfall_helpers/waterfall_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx deleted file mode 100644 index b82a8e41242235..00000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiLink } from '@elastic/eui'; -import React from 'react'; -import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; -import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; - -interface ServiceOverviewLinkProps extends APMLinkExtendProps { - serviceName: string; - environment?: string; - transactionType?: string; -} - -const persistedFilters: Array = [ - 'latencyAggregationType', -]; - -export function useServiceOverviewHref({ - serviceName, - environment, - transactionType, -}: ServiceOverviewLinkProps) { - const query = { environment, transactionType }; - return useAPMHref({ - path: `/services/${serviceName}/overview`, - persistedFilters, - query: removeUndefinedProps(query), - }); -} - -export function ServiceOverviewLink({ - serviceName, - environment, - transactionType, - ...rest -}: ServiceOverviewLinkProps) { - const href = useServiceOverviewHref({ - serviceName, - environment, - transactionType, - }); - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/index.tsx b/x-pack/plugins/apm/public/components/shared/agent_icon/index.tsx index f91eb497177828..a91ddb49d5e8f2 100644 --- a/x-pack/plugins/apm/public/components/shared/agent_icon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/index.tsx @@ -12,7 +12,7 @@ import { getAgentIcon } from './get_agent_icon'; import { useTheme } from '../../../hooks/use_theme'; interface Props { - agentName: AgentName; + agentName?: AgentName; } export function AgentIcon(props: Props) { diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.stories.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.stories.tsx new file mode 100644 index 00000000000000..31bc2f23027988 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/backend_link.stories.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentProps, ComponentType } from 'react'; +import { MockApmPluginContextWrapper } from '../../context/apm_plugin/mock_apm_plugin_context'; +import { BackendLink } from './backend_link'; + +type Args = ComponentProps; + +export default { + title: 'shared/BackendLink', + component: BackendLink, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + + + + ); + }, + ], +}; + +export const Example: Story = (args) => { + return ; +}; +Example.args = { + backendName: 'postgres', + type: 'db', + subtype: 'postgresql', +}; diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx new file mode 100644 index 00000000000000..c89a84e7c08d5f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { useApmRouter } from '../../hooks/use_apm_router'; +import { truncate } from '../../utils/style'; +import { SpanIcon } from './span_icon'; + +const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; + +interface BackendLinkProps { + backendName: string; + query: Record; + subtype?: string; + type?: string; +} + +export function BackendLink({ + backendName, + query, + subtype, + type, +}: BackendLinkProps) { + const { link } = useApmRouter(); + + return ( + + + + + + {backendName} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/service_link.stories.tsx b/x-pack/plugins/apm/public/components/shared/service_link.stories.tsx new file mode 100644 index 00000000000000..f25838a3552f43 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/service_link.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentProps, ComponentType } from 'react'; +import { MockApmPluginContextWrapper } from '../../context/apm_plugin/mock_apm_plugin_context'; +import { ServiceLink } from './service_link'; + +type Args = ComponentProps; + +export default { + title: 'shared/ServiceLink', + component: ServiceLink, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + + + + ); + }, + ], +}; + +export const Example: Story = (args) => { + return ; +}; +Example.args = { + agentName: 'java', + serviceName: 'opbeans-java', +}; diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx new file mode 100644 index 00000000000000..9a21438c08b833 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { truncate } from '../../utils/style'; +import { useApmRouter } from '../../hooks/use_apm_router'; +import { AgentIcon } from './agent_icon'; +import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; + +const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; + +interface ServiceLinkProps { + agentName?: AgentName; + query: Record; + serviceName: string; +} + +export function ServiceLink({ + agentName, + query, + serviceName, +}: ServiceLinkProps) { + const { link } = useApmRouter(); + + return ( + + + + + + {serviceName} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts index 5f4fe5df502466..93021fe177f6b9 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts @@ -35,7 +35,7 @@ const defaultSpanTypeIcons: { [key: string]: string } = { }; export const spanTypeIcons: { - [type: string]: { [subType: string]: string }; + [type: string]: { [subtype: string]: string }; } = { aws: { servicename: awsIcon, diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx index 05e4067f522a10..42be10d1468d40 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx @@ -11,11 +11,11 @@ import { getSpanIcon } from './get_span_icon'; interface Props { type?: string; - subType?: string; + subtype?: string; } -export function SpanIcon({ type, subType }: Props) { - const icon = getSpanIcon(type, subType); +export function SpanIcon({ type, subtype }: Props) { + const icon = getSpanIcon(type, subtype); - return ; + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx index 5aec7e90287457..d68b042159bb97 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx @@ -28,9 +28,9 @@ export const List: Story = () => { return ( {spanTypes.map((type) => { - const subTypes = Object.keys(spanTypeIcons[type]); - return subTypes.map((subType) => { - const id = `${type}.${subType}`; + const subtypes = Object.keys(spanTypeIcons[type]); + return subtypes.map((subtype) => { + const id = `${type}.${subtype}`; return ( @@ -45,7 +45,7 @@ export const List: Story = () => { size="s" hasShadow alt={id} - src={getSpanIcon(type, subType)} + src={getSpanIcon(type, subtype)} />

@@ -58,7 +58,7 @@ export const List: Story = () => { position="bottom" content="Icon rendered with `SpanIcon`" > - + @@ -69,7 +69,7 @@ export const List: Story = () => { }} >
span.type: {type}
-
span.subtype: {subType}
+
span.subtype: {subtype}
} diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index fb3a0475d627ad..b699b4ff7c5cfa 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -60,7 +60,9 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`; */ export const ACTION_URL = '/api/actions'; -export const ACTION_TYPES_URL = '/api/actions/list_action_types'; +export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types`; +export const CONNECTORS_URL = `${ACTION_URL}/connectors`; + export const SUPPORTED_CONNECTORS = [ `${ConnectorTypes.serviceNowITSM}`, `${ConnectorTypes.serviceNowSIR}`, diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 3d277d12d68263..89e69308d56e54 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -8,3 +8,4 @@ export * from './constants'; export * from './api'; export * from './ui/types'; +export * from './utils/connectors_api'; diff --git a/x-pack/plugins/cases/common/utils/connectors_api.ts b/x-pack/plugins/cases/common/utils/connectors_api.ts new file mode 100644 index 00000000000000..f9f85bbfb01271 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/connectors_api.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Actions and connectors API endpoint helpers + */ + +import { ACTION_URL, ACTION_TYPES_URL, CONNECTORS_URL } from '../../common'; + +/** + * + * @returns {string} Connector types endpoint + */ +export const getAllConnectorTypesUrl = (): string => ACTION_TYPES_URL; + +/** + * + * @param connectorId + * @returns {string} Execute connector endpoint + */ +export const getExecuteConnectorUrl = (connectorId: string): string => + `${ACTION_URL}/connector/${connectorId}/_execute`; + +/** + * + * @returns {string} Create connector endpoint + */ +export const getCreateConnectorUrl = (): string => `${ACTION_URL}/connector`; + +/** + * + * @returns {string} All connectors endpoint + */ +export const getAllConnectorsUrl = (): string => CONNECTORS_URL; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts b/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts index bbab8a14b5ed99..af4883ab4ba96e 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts @@ -94,7 +94,7 @@ describe('Jira API', () => { const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' }); expect(res).toEqual(issueTypesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}', signal: abortCtrl.signal, }); @@ -113,7 +113,7 @@ describe('Jira API', () => { }); expect(res).toEqual(fieldsResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}', signal: abortCtrl.signal, }); @@ -132,7 +132,7 @@ describe('Jira API', () => { }); expect(res).toEqual(issuesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', signal: abortCtrl.signal, }); @@ -151,7 +151,7 @@ describe('Jira API', () => { }); expect(res).toEqual(issuesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/api.ts b/x-pack/plugins/cases/public/components/connectors/jira/api.ts index dff3e3a5b41ab3..109d8a6794c540 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/api.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/api.ts @@ -7,10 +7,9 @@ import { HttpSetup } from 'kibana/public'; import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { IssueTypes, Fields, Issues, Issue } from './types'; -export const BASE_ACTION_API_PATH = '/api/actions'; - export interface GetIssueTypesProps { http: HttpSetup; signal: AbortSignal; @@ -18,15 +17,12 @@ export interface GetIssueTypesProps { } export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) { - return http.post>( - `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, - { - body: JSON.stringify({ - params: { subAction: 'issueTypes', subActionParams: {} }, - }), - signal, - } - ); + return http.post>(getExecuteConnectorUrl(connectorId), { + body: JSON.stringify({ + params: { subAction: 'issueTypes', subActionParams: {} }, + }), + signal, + }); } export interface GetFieldsByIssueTypeProps { @@ -42,7 +38,7 @@ export async function getFieldsByIssueType({ connectorId, id, }: GetFieldsByIssueTypeProps): Promise> { - return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return http.post(getExecuteConnectorUrl(connectorId), { body: JSON.stringify({ params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, }), @@ -63,7 +59,7 @@ export async function getIssues({ connectorId, title, }: GetIssuesTypeProps): Promise> { - return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return http.post(getExecuteConnectorUrl(connectorId), { body: JSON.stringify({ params: { subAction: 'issues', subActionParams: { title } }, }), @@ -84,7 +80,7 @@ export async function getIssue({ connectorId, id, }: GetIssueTypeProps): Promise> { - return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return http.post(getExecuteConnectorUrl(connectorId), { body: JSON.stringify({ params: { subAction: 'issue', subActionParams: { id } }, }), diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/api.ts b/x-pack/plugins/cases/public/components/connectors/resilient/api.ts index 5fec83f3039505..301d29866d3027 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/api.ts +++ b/x-pack/plugins/cases/public/components/connectors/resilient/api.ts @@ -7,6 +7,7 @@ import { HttpSetup } from 'kibana/public'; import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { ResilientIncidentTypes, ResilientSeverity } from './types'; export const BASE_ACTION_API_PATH = '/api/actions'; @@ -19,7 +20,7 @@ export interface Props { export async function getIncidentTypes({ http, signal, connectorId }: Props) { return http.post>( - `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + getExecuteConnectorUrl(connectorId), { body: JSON.stringify({ params: { subAction: 'incidentTypes', subActionParams: {} }, @@ -31,7 +32,7 @@ export async function getIncidentTypes({ http, signal, connectorId }: Props) { export async function getSeverity({ http, signal, connectorId }: Props) { return http.post>( - `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + getExecuteConnectorUrl(connectorId), { body: JSON.stringify({ params: { subAction: 'severity', subActionParams: {} }, diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts index 461823036ed21b..9ea46e5e427b78 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts @@ -31,7 +31,7 @@ describe('ServiceNow API', () => { }); expect(res).toEqual(choicesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts index e68eb18860ae30..3d9211caa9b9a1 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts @@ -7,6 +7,7 @@ import { HttpSetup } from 'kibana/public'; import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { Choice } from './types'; export const BASE_ACTION_API_PATH = '/api/actions'; @@ -19,13 +20,10 @@ export interface GetChoicesProps { } export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) { - return http.post>( - `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, - { - body: JSON.stringify({ - params: { subAction: 'getChoices', subActionParams: { fields } }, - }), - signal, - } - ); + return http.post>(getExecuteConnectorUrl(connectorId), { + body: JSON.stringify({ + params: { subAction: 'getChoices', subActionParams: { fields } }, + }), + signal, + }); } diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 30a76e28e74850..e47930e81fe6bb 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -91,7 +91,7 @@ describe('Case Configuration API', () => { test('check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`/api/actions/list_action_types`, { + expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, { method: 'GET', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index b144a874cfc53a..51a68376936af7 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -8,7 +8,6 @@ import { assign, omit } from 'lodash'; import { - ACTION_TYPES_URL, CASE_REPORTERS_URL, CASE_STATUS_URL, CASE_TAGS_URL, @@ -38,6 +37,8 @@ import { User, } from '../../common'; +import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; + import { KibanaServices } from '../common/lib/kibana'; import { @@ -351,9 +352,13 @@ export const pushCase = async ( }; export const getActionLicense = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { - method: 'GET', - signal, - }); - return response; + const response = await KibanaServices.get().http.fetch( + getAllConnectorTypesUrl(), + { + method: 'GET', + signal, + } + ); + + return convertArrayToCamelCase(response) as ActionLicense[]; }; diff --git a/x-pack/plugins/cases/public/containers/configure/api.test.ts b/x-pack/plugins/cases/public/containers/configure/api.test.ts index ad13526b41d38f..3c8c19a4f2851d 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.test.ts @@ -151,7 +151,7 @@ describe('Case Configuration API', () => { test('check url, method, signal', async () => { await fetchActionTypes({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/actions/list_action_types', { + expect(fetchMock).toHaveBeenCalledWith('/api/actions/connector_types', { method: 'GET', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index c972e2fc5c5fb1..f7d0cf1ad9aefb 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -6,8 +6,8 @@ */ import { isEmpty } from 'lodash/fp'; +import { getAllConnectorTypesUrl } from '../../../common/utils/connectors_api'; import { - ACTION_TYPES_URL, ActionConnector, ActionTypeConnector, CASE_CONFIGURE_CONNECTORS_URL, @@ -22,6 +22,7 @@ import { KibanaServices } from '../../common/lib/kibana'; import { ApiProps } from '../types'; import { + convertArrayToCamelCase, convertToCamelCase, decodeCaseConfigurationsResponse, decodeCaseConfigureResponse, @@ -60,15 +61,6 @@ export const getCaseConfigure = async ({ return null; }; -export const getConnectorMappings = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { - method: 'GET', - signal, - }); - - return response; -}; - export const postCaseConfigure = async ( caseConfiguration: CasesConfigureRequest, signal: AbortSignal @@ -105,10 +97,10 @@ export const patchCaseConfigure = async ( }; export const fetchActionTypes = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { + const response = await KibanaServices.get().http.fetch(getAllConnectorTypesUrl(), { method: 'GET', signal, }); - return response; + return convertArrayToCamelCase(response) as ActionTypeConnector[]; }; diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index edabe9c4d4a1fb..3cb013bd2e3fd1 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -18,6 +18,10 @@ import { } from '../../../common'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors'; +import { + getCreateConnectorUrl, + getExecuteConnectorUrl, +} from '../../../common/utils/connectors_api'; main(); @@ -71,7 +75,7 @@ async function handleGenGroupAlerts(argv: any) { try { const createdAction = await client.request({ - path: '/api/actions/action', + path: getCreateConnectorUrl(), method: 'POST', body: { name: 'A case connector', @@ -121,7 +125,7 @@ async function handleGenGroupAlerts(argv: any) { }; const executeResp = await client.request>({ - path: `/api/actions/action/${createdAction.data.id}/_execute`, + path: getExecuteConnectorUrl(createdAction.data.id), method: 'POST', body: { params: { diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx index 05382f3eb36ca3..2272e4925ef8da 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx @@ -35,7 +35,6 @@ const InspectFlyout = ({ uiSettings, searchSession }: InspectFlyoutProps) => { {}} options={{ readOnly: true, lineNumbers: 'off', diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index da95a0f21a0204..8d8ac11f835206 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -7,5 +7,9 @@ "requiredPlugins": ["uiActions", "embeddable", "discover"], "optionalPlugins": ["share", "kibanaLegacy", "usageCollection"], "configPath": ["xpack", "discoverEnhanced"], - "requiredBundles": ["kibanaUtils", "data"] + "requiredBundles": ["kibanaUtils", "data"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts index 32aac4a37e4c21..be05825192d356 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts @@ -16,8 +16,23 @@ afterEach(() => { jest.clearAllMocks(); }); +interface ConversionTestOptions { + /** Migration version(s) to test */ + migrationVersions: string[]; + /** The `namespace` field of the object that is being migrated */ + objectNamespace: string | undefined; + /** The `namespaces` field of the object that is being migrated */ + objectNamespaces: string[] | undefined; + /** The expected namespace in the decrypt descriptor */ + expectedDecryptDescriptorNamespace: string | undefined; + /** The expected namespace in the encrypt descriptor */ + expectedEncryptDescriptorNamespace: string | undefined; + /** The expected value for the `isTypeBeingConverted` field in the decrypt params */ + expectedIsTypeBeingConverted: boolean; +} + describe('createMigration()', () => { - const migrationContext = migrationMocks.createContext(); + const migrationContext = migrationMocks.createContext({ isSingleNamespaceType: true }); const inputType = { type: 'known-type-1', attributesToEncrypt: new Set(['firstAttr']) }; const migrationType = { type: 'known-type-1', @@ -100,7 +115,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( @@ -157,7 +172,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).not.toHaveBeenCalled(); @@ -209,7 +224,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(encryptionSavedObjectService.stripOrDecryptAttributesSync).not.toHaveBeenCalled(); @@ -273,7 +288,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).toHaveBeenCalled(); @@ -331,7 +346,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).toHaveBeenCalled(); @@ -383,7 +398,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).toHaveBeenCalled(); @@ -435,7 +450,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).toHaveBeenCalled(); @@ -495,7 +510,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(migrationFunc).toHaveBeenCalled(); @@ -551,7 +566,7 @@ describe('createMigration()', () => { namespace: 'namespace', }, attributes, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( @@ -564,14 +579,15 @@ describe('createMigration()', () => { ); }); - describe('uses the object `namespaces` field to populate the descriptor when the migration context indicates this type is being converted', () => { + describe('handles conversion to a multi-namespace type (convertToMultiNamespaceVersion migration context field)', () => { const doTest = ({ + migrationVersions, objectNamespace, - decryptDescriptorNamespace, - }: { - objectNamespace: string | undefined; - decryptDescriptorNamespace: string | undefined; - }) => { + objectNamespaces, + expectedDecryptDescriptorNamespace, + expectedEncryptDescriptorNamespace, + expectedIsTypeBeingConverted, + }: ConversionTestOptions) => { const instantiateServiceWithLegacyType = jest.fn(() => encryptedSavedObjectsServiceMock.create() ); @@ -586,56 +602,150 @@ describe('createMigration()', () => { }, migration: (doc) => doc, }); - - const attributes = { - firstAttr: 'first_attr', - }; - - encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); - encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); - - noopMigration( - { - id: '123', - type: 'known-type-1', - namespaces: objectNamespace ? [objectNamespace] : [], + const attributes = { firstAttr: 'first_attr' }; + + migrationVersions.forEach((migrationVersion, i) => { + encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + noopMigration( + { + id: '123', + originId: 'some-origin-id', + type: 'known-type-1', + namespace: objectNamespace, + namespaces: objectNamespaces, + attributes, + }, + migrationMocks.createContext({ + migrationVersion, // Any migrationVersion <= convertToMultiNamespaceTypeVersion (8.0.0) indicates this type is being converted + convertToMultiNamespaceTypeVersion: '8.0.0', + isSingleNamespaceType: false, // in practice, this can never be true if convertToMultiNamespaceTypeVersion is defined + }) + ); + expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenNthCalledWith( + i + 1, + { id: '123', type: 'known-type-1', namespace: expectedDecryptDescriptorNamespace }, attributes, - }, - migrationMocks.createContext({ - migrationVersion: '8.0.0', - convertToMultiNamespaceTypeVersion: '8.0.0', - }) - ); - - expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenCalledWith( - { - id: '123', - type: 'known-type-1', - namespace: decryptDescriptorNamespace, - }, - attributes, - { convertToMultiNamespaceType: true } - ); - - expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( - { - id: '123', - type: 'known-type-1', - }, - attributes - ); + { isTypeBeingConverted: expectedIsTypeBeingConverted, originId: 'some-origin-id' } // decrypt parameters + ); + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenNthCalledWith( + i + 1, + { id: '123', type: 'known-type-1', namespace: expectedEncryptDescriptorNamespace }, + attributes + ); + }); }; - it('when namespaces is an empty array', () => { - doTest({ objectNamespace: undefined, decryptDescriptorNamespace: undefined }); - }); - - it('when the first namespace element is "default"', () => { - doTest({ objectNamespace: 'default', decryptDescriptorNamespace: undefined }); + describe('when migrationVersion <= convertToMultiNamespaceVersion, it sets the descriptors and decrypt parameters appropriately', () => { + const migrationVersions = ['7.99.99', '8.0.0']; + [undefined, 'foo'].forEach((objectNamespace) => { + // In the test cases below, we test what will happen if an object has both `namespace` and `namespaces` fields. This will not happen + // in normal operation, as when Kibana converts an object from a single- to a multi-namespace type, it removes the `namespace` field + // and adds the `namespaces` field. The tests below are for completeness. + const namespaceDescription = objectNamespace ? 'defined' : undefined; + + it(`when namespaces is undefined and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: undefined, + expectedDecryptDescriptorNamespace: objectNamespace, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: true, + }); + }); + + it(`when namespaces is an empty array and namespace is ${namespaceDescription}`, () => { + // The `namespaces` field should never be an empty array, but we test for it anyway. In this case, we fall back to attempting to + // decrypt with the `namespace` field; if that doesn't work, the ESO service will try again without it. + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: [], + expectedDecryptDescriptorNamespace: objectNamespace, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: true, + }); + }); + + it(`when namespaces is a non-empty array (default space) and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: ['default', 'additional-spaces-are-ignored'], + expectedDecryptDescriptorNamespace: undefined, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: true, + }); + }); + + it(`when namespaces is a non-empty array (custom space) and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: ['custom', 'additional-spaces-are-ignored'], + expectedDecryptDescriptorNamespace: 'custom', + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: true, + }); + }); + }); }); - it('when the first namespace element is another string', () => { - doTest({ objectNamespace: 'foo', decryptDescriptorNamespace: 'foo' }); + describe('when migrationVersion > convertToMultiNamespaceVersion, it sets the descriptors and decrypt parameters appropriately', () => { + const migrationVersions = ['8.0.1']; + [undefined, 'foo'].forEach((objectNamespace) => { + // In the test cases below, we test what will happen if an object has both `namespace` and `namespaces` fields. This will not happen + // in normal operation, as when Kibana converts an object from a single- to a multi-namespace type, it removes the `namespace` field + // and adds the `namespaces` field. The tests below are for completeness. + const namespaceDescription = objectNamespace ? 'defined' : undefined; + + it(`when namespaces is undefined and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: undefined, + expectedDecryptDescriptorNamespace: undefined, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: false, + }); + }); + + it(`when namespaces is an empty array and namespace is ${namespaceDescription}`, () => { + // The `namespaces` field should never be an empty array, but we test for it anyway. In this case, we fall back to attempting to + // decrypt with the `namespace` field; if that doesn't work, the ESO service will try again without it. + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: [], + expectedDecryptDescriptorNamespace: undefined, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: false, + }); + }); + + it(`when namespaces is a non-empty array (default space) and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: ['default', 'additional-spaces-are-ignored'], + expectedDecryptDescriptorNamespace: undefined, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: false, + }); + }); + + it(`when namespaces is a non-empty array (custom space) and namespace is ${namespaceDescription}`, () => { + doTest({ + migrationVersions, + objectNamespace, + objectNamespaces: ['custom', 'additional-spaces-are-ignored'], + expectedDecryptDescriptorNamespace: undefined, + expectedEncryptDescriptorNamespace: undefined, + expectedIsTypeBeingConverted: false, + }); + }); + }); }); }); }); @@ -754,7 +864,7 @@ describe('createMigration()', () => { firstAttr: '#####', nonEncryptedAttr: 'non encrypted', }, - { convertToMultiNamespaceType: false } + { isTypeBeingConverted: false } ); expect(serviceWithMigrationLegacyType.encryptAttributesSync).toHaveBeenCalledWith( diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts index b9e6dcf7109245..18786e0f9d1c63 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts @@ -5,6 +5,8 @@ * 2.0. */ +import semver from 'semver'; + import type { SavedObjectMigrationContext, SavedObjectMigrationFn, @@ -77,19 +79,44 @@ export const getCreateMigration = ( return encryptedDoc; } + // After it is converted, the object's old ID is stored in the `originId` field. In addition, objects that are imported a certain way + // may have this field set, but it would not be necessary to use this to decrypt saved object attributes. + const { type, id, originId } = encryptedDoc; + + // If an object is slated to be converted, it should be decrypted flexibly: + // * If this is an index migration: + // a. If there is one or more pending migration _before_ the conversion, the object will be decrypted and re-encrypted with its + // namespace in the descriptor. Then, after the conversion, the object will be decrypted with its namespace and old ID in the + // descriptor and re-encrypted with its new ID (without a namespace) in the descriptor. + // b. If there are no pending migrations before the conversion, then after the conversion the object will be decrypted with its + // namespace and old ID in the descriptor and re-encrypted with its new ID (without a namespace) in the descriptor. + // * If this is *not* an index migration, then it is a single document migration. In that case, the object will be decrypted and + // re-encrypted without a namespace in the descriptor. + // To account for these different scenarios, when this field is set, the ESO service will attempt several different permutations of + // the descriptor when decrypting the object. + const isTypeBeingConverted = + !!context.convertToMultiNamespaceTypeVersion && + semver.lte(context.migrationVersion, context.convertToMultiNamespaceTypeVersion); + + // This approximates the behavior of getDescriptorNamespace(); the intent is that if there is ever a case where a multi-namespace object + // has the `namespace` field, it will not be encrypted with that field in its descriptor. It would be preferable to rely on + // getDescriptorNamespace() here, but that requires the SO type registry which can only be retrieved from a promise, and this is not an + // async function + const descriptorNamespace = context.isSingleNamespaceType ? encryptedDoc.namespace : undefined; + let decryptDescriptorNamespace = descriptorNamespace; + // If an object has been converted right before this migration function is called, it will no longer have a `namespace` field, but it // will have a `namespaces` field; in that case, the first/only element in that array should be used as the namespace in the descriptor // during decryption. - const convertToMultiNamespaceType = - context.convertToMultiNamespaceTypeVersion === context.migrationVersion; - const decryptDescriptorNamespace = convertToMultiNamespaceType - ? normalizeNamespace(encryptedDoc.namespaces?.[0]) // `namespaces` contains string values, but we need to normalize this to the namespace ID representation - : encryptedDoc.namespace; + if (isTypeBeingConverted) { + decryptDescriptorNamespace = encryptedDoc.namespaces?.length + ? normalizeNamespace(encryptedDoc.namespaces[0]) // `namespaces` contains string values, but we need to normalize this to the namespace ID representation + : encryptedDoc.namespace; + } - const { id, type } = encryptedDoc; // These descriptors might have a `namespace` that is undefined. That is expected for multi-namespace and namespace-agnostic types. const decryptDescriptor = { id, type, namespace: decryptDescriptorNamespace }; - const encryptDescriptor = { id, type, namespace: encryptedDoc.namespace }; + const encryptDescriptor = { id, type, namespace: descriptorNamespace }; // decrypt the attributes using the input type definition // if an error occurs during decryption, use the shouldMigrateIfDecryptionFails flag @@ -98,7 +125,8 @@ export const getCreateMigration = ( const documentToMigrate = mapAttributes(encryptedDoc, (inputAttributes) => { try { return inputService.decryptAttributesSync(decryptDescriptor, inputAttributes, { - convertToMultiNamespaceType, + isTypeBeingConverted, + originId, }); } catch (err) { if (!shouldMigrateIfDecryptionFails || !(err instanceof EncryptionError)) { @@ -109,7 +137,8 @@ export const getCreateMigration = ( `Decryption failed for encrypted Saved Object "${encryptedDoc.id}" of type "${encryptedDoc.type}" with error: ${err.message}. Encrypted attributes have been stripped from the original document and migration will be applied but this may cause errors later on.` ); return inputService.stripOrDecryptAttributesSync(decryptDescriptor, inputAttributes, { - convertToMultiNamespaceType, + isTypeBeingConverted, + originId, }).attributes; } }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index 0625199eeed63d..15de21999fba32 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -1024,53 +1024,163 @@ describe('#decryptAttributes', () => { ); }); - it('retries decryption without namespace if incorrect namespace is provided and convertToMultiNamespaceType option is enabled', async () => { - const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + describe('with isTypeBeingConverted option', () => { + it('retries decryption without namespace', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; - service.registerType({ - type: 'known-type-1', - attributesToEncrypt: new Set(['attrThree']), + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt without the namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + mockUser + ); }); - const encryptedAttributes = service.encryptAttributesSync( - { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption - attributes - ); - expect(encryptedAttributes).toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: expect.not.stringMatching(/^three$/), + it('retries decryption without originId (old object ID)', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // old object ID was not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true, originId: 'old-object-id' } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the current object ID in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + mockUser + ); }); - const mockUser = mockAuthenticatedUser(); - await expect( - service.decryptAttributes( + it('retries decryption without namespace *and* without originId (old object ID)', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace and old object ID were not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true, originId: 'old-object-id' } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(4); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the old object ID and no namespace in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 3, // first attempted to decrypt with the current object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 4, // then attempted to decrypt with the current object ID and no namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - encryptedAttributes, - { user: mockUser, convertToMultiNamespaceType: true } - ) - ).resolves.toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: 'three', + mockUser + ); }); - expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); - expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( - 1, // first attempted to decrypt with the namespace in the descriptor (fail) - expect.anything(), - `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( - 2, // then attempted to decrypt without the namespace in the descriptor (success) - expect.anything(), - `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( - ['attrThree'], - { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - mockUser - ); }); it('decrypts even if no attributes are included into AAD', async () => { @@ -1289,7 +1399,7 @@ describe('#decryptAttributes', () => { service.decryptAttributes( { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, encryptedAttributes, - { user: mockUser, convertToMultiNamespaceType: true } + { user: mockUser, isTypeBeingConverted: true } ) ).rejects.toThrowError(EncryptionError); expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); @@ -2002,53 +2112,163 @@ describe('#decryptAttributesSync', () => { }); }); - it('retries decryption without namespace if incorrect namespace is provided and convertToMultiNamespaceType option is enabled', () => { - const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + describe('with isTypeBeingConverted option', () => { + it('retries decryption without namespace', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; - service.registerType({ - type: 'known-type-1', - attributesToEncrypt: new Set(['attrThree']), + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true } + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decryptSync).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt without the namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + mockUser + ); }); - const encryptedAttributes = service.encryptAttributesSync( - { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption - attributes - ); - expect(encryptedAttributes).toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: expect.not.stringMatching(/^three$/), + it('retries decryption without originId (old object ID)', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // old object ID was not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true, originId: 'old-object-id' } + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decryptSync).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the current object ID in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + mockUser + ); }); - const mockUser = mockAuthenticatedUser(); - expect( - service.decryptAttributesSync( + it('retries decryption without namespace *and* without originId (old object ID)', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace and old object ID were not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, isTypeBeingConverted: true, originId: 'old-object-id' } + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decryptSync).toHaveBeenCalledTimes(4); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the old object ID and no namespace in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 3, // first attempted to decrypt with the current object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( + 4, // then attempted to decrypt with the current object ID and no namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - encryptedAttributes, - { user: mockUser, convertToMultiNamespaceType: true } - ) - ).toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: 'three', + mockUser + ); }); - expect(mockNodeCrypto.decryptSync).toHaveBeenCalledTimes(2); - expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( - 1, // first attempted to decrypt with the namespace in the descriptor (fail) - expect.anything(), - `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockNodeCrypto.decryptSync).toHaveBeenNthCalledWith( - 2, // then attempted to decrypt without the namespace in the descriptor (success) - expect.anything(), - `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( - ['attrThree'], - { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - mockUser - ); }); it('decrypts even if no attributes are included into AAD', () => { @@ -2214,7 +2434,7 @@ describe('#decryptAttributesSync', () => { service.decryptAttributesSync( { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, encryptedAttributes, - { user: mockUser, convertToMultiNamespaceType: true } + { user: mockUser, isTypeBeingConverted: true } ) ).toThrowError(EncryptionError); expect(mockNodeCrypto.decryptSync).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index cc1a8414924c39..0cf775b88e7d5b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -70,7 +70,12 @@ interface DecryptParameters extends CommonParameters { * the object was previously encrypted with its namespace in the descriptor portion of the AAD; on the other hand, if the object is being * decrypted during object migration, the object was never encrypted with its namespace in the descriptor portion of the AAD. */ - convertToMultiNamespaceType?: boolean; + isTypeBeingConverted?: boolean; + /** + * If the originId (old object ID) is present and the object is being converted from a single-namespace type to a multi-namespace type, + * we will attempt to decrypt with both the old object ID and the current object ID. + */ + originId?: string; } interface EncryptedSavedObjectsServiceOptions { @@ -528,11 +533,23 @@ export class EncryptedSavedObjectsService { ); } if (!encryptionAADs.length) { - encryptionAADs.push(this.getAAD(typeDefinition, descriptor, attributes)); - if (params?.convertToMultiNamespaceType && descriptor.namespace) { - // This is happening during a migration; create an alternate AAD for decrypting the object attributes by stripping out the namespace from the descriptor. - const { namespace, ...alternateDescriptor } = descriptor; - encryptionAADs.push(this.getAAD(typeDefinition, alternateDescriptor, attributes)); + if (params?.isTypeBeingConverted) { + // The object is either pending conversion to a multi-namespace type, or it was just converted. We may need to attempt to decrypt + // it with several different descriptors depending upon how the migrations are structured, and whether this is a full index + // migration or a single document migration. Note that the originId is set either when the document is converted _or_ when it is + // imported with "createNewCopies: false", so we have to try with and without it. + const decryptDescriptors = params.originId + ? [{ ...descriptor, id: params.originId }, descriptor] + : [descriptor]; + for (const decryptDescriptor of decryptDescriptors) { + encryptionAADs.push(this.getAAD(typeDefinition, decryptDescriptor, attributes)); + if (descriptor.namespace) { + const { namespace, ...alternateDescriptor } = decryptDescriptor; + encryptionAADs.push(this.getAAD(typeDefinition, alternateDescriptor, attributes)); + } + } + } else { + encryptionAADs.push(this.getAAD(typeDefinition, descriptor, attributes)); } } try { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index e9ef3b3cfee283..9b5d6090f2f8a7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -24,6 +24,7 @@ import { import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Schema, SchemaType } from '../../../shared/schema/types'; +import { InlineEditableTable } from '../../../shared/tables/inline_editable_table'; import { ReorderableTable } from '../../../shared/tables/reorderable_table'; import { Result } from '../result'; @@ -31,6 +32,16 @@ const NO_ITEMS = ( No Items} body={

No Items

} /> ); +// For the InlineEditableTable +// Since InlineEditableTable caches handlers, we need to store this globally somewhere rather than just in useState +interface Foo { + id: number; + foo: string; + bar: string; +} +let globalItems: Foo[] = []; +const getLastItems = () => globalItems; + export const Library: React.FC = () => { const props = { isMetaEngine: false, @@ -91,6 +102,67 @@ export const Library: React.FC = () => { }, ]; + // For the InlineEditableTable + const [items, setItems] = useState([ + { id: 1, foo: 'foo1', bar: '10' }, + { id: 2, foo: 'foo2', bar: '10' }, + ]); + globalItems = items; + const columns = [ + { + field: 'foo', + name: 'Foo', + render: (item: Foo) =>
{item.foo}
, + editingRender: (item: Foo, onChange: (value: string) => void) => ( + onChange(e.target.value)} /> + ), + }, + { + field: 'bar', + name: 'Bar (Must be a number)', + render: (item: Foo) =>
{item.bar}
, + editingRender: (item: Foo, onChange: (value: string) => void) => ( + onChange(e.target.value)} /> + ), + }, + ]; + const onAdd = (item: Foo, onSuccess: () => void) => { + const highestId = Math.max(...getLastItems().map((i) => i.id)); + setItems([ + ...getLastItems(), + { + ...item, + id: highestId + 1, + }, + ]); + onSuccess(); + }; + const onDelete = (item: Foo) => { + setItems(getLastItems().filter((i) => i.id !== item.id)); + }; + const onUpdate = (item: Foo, onSuccess: () => void) => { + setItems( + getLastItems().map((i) => { + if (item.id === i.id) return item; + return i; + }) + ); + onSuccess(); + }; + const validateItem = (item: Foo) => { + let isValidNumber = false; + const num = parseInt(item.bar, 10); + if (!isNaN(num)) isValidNumber = true; + + if (isValidNumber) return {}; + return { + bar: 'Bar must be a valid number', + }; + }; + const onReorder = (newItems: Foo[]) => { + setItems(newItems); + }; + return ( <> @@ -353,8 +425,99 @@ export const Library: React.FC = () => { { name: 'Whatever', render: (item) =>
Whatever
}, ]} /> + + +

InlineEditableTable

+
+ + +

With uneditable items

+
+ + + + + +

Can delete last item

+
+ + { + return ( + No Items} + body={} + /> + ); + }} + /> + + + +

Cannot delete last item

+
+ + + + + +

When isLoading is true

+
+ + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx new file mode 100644 index 00000000000000..6328b01cd2be7a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { ActionColumn } from './action_column'; + +const requiredParams = { + displayedItems: [], + isActivelyEditing: () => false, + item: { id: 1 }, +}; + +describe('ActionColumn', () => { + const mockValues = { + doesEditingItemValueContainEmptyProperty: false, + editingItemId: 1, + formErrors: [], + isEditing: false, + isEditingUnsavedItem: false, + }; + const mockActions = { + editExistingItem: jest.fn(), + deleteItem: jest.fn(), + doneEditing: jest.fn(), + saveExistingItem: jest.fn(), + saveNewItem: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(mockValues); + setMockActions(mockActions); + }); + + it('renders', () => { + const wrapper = shallow( + false} + isLoading={false} + item={{ id: 1 }} + canRemoveLastItem={false} + lastItemWarning="I am a warning" + uneditableItems={[]} + /> + ); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('renders nothing if the item is an uneditableItem', () => { + const item = { id: 1 }; + const wrapper = shallow( + false} + isLoading={false} + item={item} + canRemoveLastItem={false} + lastItemWarning="I am a warning" + uneditableItems={[item]} + /> + ); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + describe('when the user is actively editing', () => { + const isActivelyEditing = () => true; + const activelyEditingParams = { + ...requiredParams, + isActivelyEditing, + }; + + describe('it renders a save button', () => { + const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="saveButton"]'); + + it('which is disabled if data is loading', () => { + const wrapper = shallow(); + expect(subject(wrapper).prop('disabled')).toBe(true); + }); + + it('which is disabled if there are form errors', () => { + setMockValues({ + ...mockValues, + formErrors: ['I am an error'], + }); + + const wrapper = shallow(); + expect(subject(wrapper).prop('disabled')).toBe(true); + }); + + it('which is disabled if the item value contains an empty property', () => { + setMockValues({ + ...mockValues, + doesEditingItemValueContainEmptyProperty: true, + }); + + const wrapper = shallow(); + expect(subject(wrapper).prop('disabled')).toBe(true); + }); + + it('which calls saveNewItem when clicked if the user is editing an unsaved item', () => { + setMockValues({ + ...mockValues, + isEditingUnsavedItem: true, + }); + + const wrapper = shallow(); + subject(wrapper).simulate('click'); + expect(mockActions.saveNewItem).toHaveBeenCalled(); + }); + + it('which calls saveExistingItem when clicked if the user is NOT editing an unsaved item', () => { + setMockValues({ + ...mockValues, + isEditingUnsavedItem: false, + }); + + const wrapper = shallow(); + subject(wrapper).simulate('click'); + expect(mockActions.saveExistingItem).toHaveBeenCalled(); + }); + }); + + describe('it renders a cancel button', () => { + const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="cancelButton"]'); + + it('which is disabled if data is loading', () => { + const wrapper = shallow(); + expect(subject(wrapper).prop('disabled')).toBe(true); + }); + + it('which calls doneEditing when clicked', () => { + const wrapper = shallow(); + subject(wrapper).simulate('click'); + expect(mockActions.doneEditing).toHaveBeenCalled(); + }); + }); + }); + + describe('when the user is NOT actively editing', () => { + const item = { id: 2 }; + + const mockValuesWhereUserIsNotActivelyEditing = { + ...mockValues, + isEditing: false, + }; + + beforeEach(() => { + setMockValues(mockValuesWhereUserIsNotActivelyEditing); + }); + + describe('it renders an edit button', () => { + const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="editButton"]'); + + it('which calls editExistingItem when clicked', () => { + const wrapper = shallow(); + subject(wrapper).simulate('click'); + expect(mockActions.editExistingItem).toHaveBeenCalledWith(item); + }); + }); + + describe('it renders an delete button', () => { + const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="deleteButton"]'); + + it('which calls deleteItem when clicked', () => { + const wrapper = shallow(); + subject(wrapper).simulate('click'); + expect(mockActions.deleteItem).toHaveBeenCalledWith(item); + }); + + it('which does not render if candRemoveLastItem is prevented and this is the last item', () => { + const wrapper = shallow( + + ); + expect(subject(wrapper).exists()).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx new file mode 100644 index 00000000000000..fe0ada8a2f914c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +import { + CANCEL_BUTTON_LABEL, + DELETE_BUTTON_LABEL, + EDIT_BUTTON_LABEL, + SAVE_BUTTON_LABEL, +} from '../../constants'; + +import { InlineEditableTableLogic } from './inline_editable_table_logic'; +import { ItemWithAnID } from './types'; + +interface ActionColumnProps { + displayedItems: Item[]; + isActivelyEditing: (i: Item) => boolean; + isLoading?: boolean; + item: Item; + canRemoveLastItem?: boolean; + lastItemWarning?: string; + uneditableItems?: Item[]; +} + +export const ActionColumn = ({ + displayedItems, + isActivelyEditing, + isLoading = false, + item, + canRemoveLastItem, + lastItemWarning, + uneditableItems, +}: ActionColumnProps) => { + const { doesEditingItemValueContainEmptyProperty, formErrors, isEditingUnsavedItem } = useValues( + InlineEditableTableLogic + ); + const { editExistingItem, deleteItem, doneEditing, saveExistingItem, saveNewItem } = useActions( + InlineEditableTableLogic + ); + + if (uneditableItems?.includes(item)) { + return null; + } + + if (isActivelyEditing(item)) { + return ( + + + 0 || + doesEditingItemValueContainEmptyProperty + } + > + {SAVE_BUTTON_LABEL} + + + + + {CANCEL_BUTTON_LABEL} + + + + ); + } + + return ( + + + editExistingItem(item)} + > + {EDIT_BUTTON_LABEL} + + + + {!canRemoveLastItem && displayedItems.length === 1 ? ( + + + {DELETE_BUTTON_LABEL} + + + ) : ( + deleteItem(item)}> + {DELETE_BUTTON_LABEL} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/constants.ts new file mode 100644 index 00000000000000..43a5a5b1df9e6a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const EMPTY_ITEM = { id: null }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx new file mode 100644 index 00000000000000..43ced1bd87492e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiFormRow } from '@elastic/eui'; + +import { EditingColumn } from './editing_column'; + +describe('EditingColumn', () => { + const column = { + name: 'foo', + field: 'foo', + render: jest.fn(), + editingRender: jest.fn().mockReturnValue(
), + }; + + const requiredProps = { + column, + }; + + const mockValues = { + formErrors: [], + editingItemValue: { id: 1 }, + }; + + const mockActions = { + setEditingItemValue: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(mockActions); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + describe('when there is a form error for this field', () => { + let wrapper: ShallowWrapper; + beforeEach(() => { + setMockValues({ + ...mockValues, + formErrors: { + foo: 'I am an error for foo and should be displayed', + }, + }); + + wrapper = shallow( + + ); + }); + + it('renders form errors for this field if any are present', () => { + expect(shallow(wrapper.find(EuiFormRow).prop('helpText') as any).html()).toContain( + 'I am an error for foo and should be displayed' + ); + }); + + it('renders as invalid', () => { + expect(wrapper.find(EuiFormRow).prop('isInvalid')).toBe(true); + }); + }); + + it('renders nothing if there is no editingItemValue in state', () => { + setMockValues({ + ...mockValues, + editingItemValue: null, + }); + + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the column\'s "editing" view (editingRender)', () => { + setMockValues({ + ...mockValues, + editingItemValue: { id: 1, foo: 'foo', bar: 'bar' }, + formErrors: { foo: ['I am an error for foo'] }, + }); + + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="editing-view"]').exists()).toBe(true); + + expect(column.editingRender).toHaveBeenCalled(); + // The render function is provided with the item currently being edited for rendering + expect(column.editingRender.mock.calls[0][0]).toEqual({ id: 1, foo: 'foo', bar: 'bar' }); + + // The render function is provided with a callback function to save the value once editing is finished + const callback = column.editingRender.mock.calls[0][1]; + callback('someNewValue'); + expect(mockActions.setEditingItemValue).toHaveBeenCalledWith({ + id: 1, + // foo is the 'field' this column is associated with, so that field is updated with the new value + foo: 'someNewValue', + bar: 'bar', + }); + + // The render function is provided with additional properties + expect(column.editingRender.mock.calls[0][2]).toEqual({ + isInvalid: true, // Because there errors for 'foo' + isLoading: true, // Because isLoading was passed as true to this prop + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx new file mode 100644 index 00000000000000..43a1d5de9b44f4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiFormRow, EuiText } from '@elastic/eui'; + +import { InlineEditableTableLogic } from './inline_editable_table_logic'; +import { InlineEditableTableColumn, ItemWithAnID } from './types'; + +interface EditingColumnProps { + column: InlineEditableTableColumn; + isLoading?: boolean; +} + +export const EditingColumn = ({ + column, + isLoading = false, +}: EditingColumnProps) => { + const { formErrors, editingItemValue } = useValues(InlineEditableTableLogic); + const { setEditingItemValue } = useActions(InlineEditableTableLogic); + + if (!editingItemValue) return null; + + return ( + + {formErrors[column.field]} + + } + isInvalid={!!formErrors[column.field]} + > + <> + {column.editingRender( + editingItemValue as Item, // TODO we shouldn't need to cast this? + (newValue) => { + setEditingItemValue({ + ...editingItemValue, + [column.field]: newValue, + }); + }, + { + isInvalid: !!formErrors[column.field], + isLoading, + } + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.test.tsx new file mode 100644 index 00000000000000..6fccdfd327df4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { Column } from '../reorderable_table/types'; + +import { ActionColumn } from './action_column'; +import { EditingColumn } from './editing_column'; +import { getUpdatedColumns } from './get_updated_columns'; +import { InlineEditableTableColumn } from './types'; + +interface Foo { + id: number; +} + +describe('getUpdatedColumns', () => { + const displayedItems: Foo[] = []; + const canRemoveLastItem = true; + const lastItemWarning = 'I am a warning'; + const uneditableItems: Foo[] = []; + const item = { id: 1 }; + + describe('it takes an array of InlineEditableTableColumn columns and turns them into ReorderableTable Columns', () => { + const columns: Array> = [ + { + name: 'Foo', + editingRender: jest.fn(), + render: jest.fn(), + field: 'foo', + }, + { + name: 'Bar', + editingRender: jest.fn(), + render: jest.fn(), + field: 'bar', + }, + ]; + let newColumns: Array> = []; + + beforeAll(() => { + newColumns = getUpdatedColumns({ + columns, + displayedItems, + canRemoveLastItem, + lastItemWarning, + uneditableItems, + isActivelyEditing: () => true, + }); + }); + + it('converts the columns to Column objects', () => { + expect(newColumns[0]).toEqual({ + name: 'Foo', + render: expect.any(Function), + }); + expect(newColumns[1]).toEqual({ + name: 'Bar', + render: expect.any(Function), + }); + }); + + it('appends an action column at the end', () => { + expect(newColumns[2]).toEqual({ + flexBasis: '200px', + flexGrow: 0, + render: expect.any(Function), + }); + + const renderResult = newColumns[2].render(item); + const wrapper = shallow(
{renderResult}
); + const actionColumn = wrapper.find(ActionColumn); + expect(actionColumn.props()).toEqual({ + isActivelyEditing: expect.any(Function), + displayedItems, + isLoading: false, + canRemoveLastItem, + lastItemWarning, + uneditableItems, + item, + }); + }); + }); + + describe("the converted column's render prop", () => { + const columns: Array> = [ + { + name: 'Foo', + editingRender: jest.fn(), + render: jest.fn(), + field: 'foo', + }, + ]; + + it("renders with the passed column's editingRender function when the user is actively editing", () => { + const newColumns = getUpdatedColumns({ + columns, + displayedItems, + isLoading: true, + isActivelyEditing: () => true, + }); + + const renderResult = newColumns[0].render(item); + const wrapper = shallow(
{renderResult}
); + const column = wrapper.find(EditingColumn); + expect(column.props()).toEqual({ + column: columns[0], + isLoading: true, + }); + }); + + it("renders with the passed column's render function when the user is NOT actively editing", () => { + const newColumns = getUpdatedColumns({ + columns, + displayedItems, + isActivelyEditing: () => false, + }); + + newColumns[0].render(item); + expect(columns[0].render).toHaveBeenCalledWith(item); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.tsx new file mode 100644 index 00000000000000..8a61e12442f7cd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/get_updated_columns.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Column } from '../reorderable_table/types'; + +import { ActionColumn } from './action_column'; +import { EditingColumn } from './editing_column'; +import { ItemWithAnID, InlineEditableTableColumn } from './types'; + +interface GetUpdatedColumnProps { + columns: Array>; + displayedItems: Item[]; + isActivelyEditing: (item: Item) => boolean; + canRemoveLastItem?: boolean; + isLoading?: boolean; + lastItemWarning?: string; + uneditableItems?: Item[]; +} + +export const getUpdatedColumns = ({ + columns, + displayedItems, + isActivelyEditing, + canRemoveLastItem, + isLoading = false, + lastItemWarning, + uneditableItems, +}: GetUpdatedColumnProps): Array> => { + return [ + ...columns.map((column) => { + const newColumn: Column = { + name: column.name, + render: (item: Item) => { + if (isActivelyEditing(item)) { + return ; + } + return column.render(item); + }, + }; + return newColumn; + }), + { + flexBasis: '200px', + flexGrow: 0, + render: (item: Item) => ( + + ), + }, + ]; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/index.ts new file mode 100644 index 00000000000000..b55fd0df83ed05 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { InlineEditableTable } from './inline_editable_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx new file mode 100644 index 00000000000000..ab59616e9ce786 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; +import { BindLogic } from 'kea'; + +import { ReorderableTable } from '../reorderable_table'; + +jest.mock('./get_updated_columns', () => ({ + getUpdatedColumns: jest.fn(), +})); +import { getUpdatedColumns } from './get_updated_columns'; + +import { InlineEditableTable, InlineEditableTableContents } from './inline_editable_table'; +import { InlineEditableTableLogic } from './inline_editable_table_logic'; + +const items = [{ id: 1 }, { id: 2 }]; +const requiredParams = { + columns: [], + items, + title: 'Some Title', +}; + +interface Foo { + id: number; +} + +describe('InlineEditableTable', () => { + const mockValues = {}; + const mockActions = {}; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(mockValues); + setMockActions(mockActions); + }); + + it('wraps the table in a bound logic, and passes through only required props to the underlying component', () => { + const instanceId = 'MyInstance'; + const onAdd = jest.fn(); + const onDelete = jest.fn(); + const onReorder = jest.fn(); + const onUpdate = jest.fn(); + const transformItem = jest.fn(); + const validateItem = jest.fn(); + const wrapper = shallow( + + ); + const bindLogic = wrapper.find(BindLogic); + expect(bindLogic.props()).toEqual( + expect.objectContaining({ + logic: InlineEditableTableLogic, + props: { + columns: requiredParams.columns, + instanceId, + onAdd, + onDelete, + onReorder, + onUpdate, + transformItem, + validateItem, + }, + }) + ); + + expect(bindLogic.children().props()).toEqual(requiredParams); + }); + + it('renders a ReorderableTable', () => { + const wrapper = shallow(); + const reorderableTable = wrapper.find(ReorderableTable); + expect(reorderableTable.exists()).toBe(true); + expect(reorderableTable.prop('items')).toEqual(items); + expect(wrapper.find('[data-test-subj="actionButton"]').children().text()).toEqual('New row'); + }); + + it('renders a description if one is provided', () => { + const wrapper = shallow( + Some Description

} /> + ); + expect(wrapper.find('[data-test-subj="description"]').exists()).toBe(true); + }); + + it('can specify items in the table that are uneditable', () => { + const uneditableItems = [{ id: 3 }]; + const wrapper = shallow( + + ); + expect(wrapper.find(ReorderableTable).prop('unreorderableItems')).toBe(uneditableItems); + }); + + it('can apply an additional className', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('.editableTable.myTestClassName').exists()).toBe(true); + }); + + it('will use the value of addButtonText as custom text on the New Row button', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="actionButton"]').children().text()).toEqual( + 'Add a new row custom text' + ); + }); + + describe('when a user is editing an unsaved item', () => { + beforeEach(() => setMockValues({ ...mockValues, isEditingUnsavedItem: true })); + + it('will change the displayed items to END with an empty item', () => { + const wrapper = shallow(); + expect(wrapper.find(ReorderableTable).prop('items')).toEqual([...items, { id: null }]); + }); + + it('will change the displayed items to START with an empty item when there are uneditableItems', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ReorderableTable).prop('items')).toEqual([{ id: null }, ...items]); + }); + }); + + it('will style the row that is currently being edited', () => { + setMockValues({ ...mockValues, isEditing: true, editingItemId: 2 }); + const itemList = [{ id: 1 }, { id: 2 }]; + const wrapper = shallow(); + const rowProps = wrapper.find(ReorderableTable).prop('rowProps') as (item: any) => object; + expect(rowProps(items[0])).toEqual({ className: '' }); + // Since editingItemId is 2 and the second item (position 1) in item list has an id of 2, it gets this class + expect(rowProps(items[1])).toEqual({ className: 'is-being-edited' }); + }); + + it('will update the passed columns and pass them through to the underlying table', () => { + const updatedColumns = {}; + const canRemoveLastItem = true; + const isLoading = true; + const lastItemWarning = 'A warning'; + const uneditableItems: Foo[] = []; + + (getUpdatedColumns as jest.Mock).mockReturnValue(updatedColumns); + const wrapper = shallow( + + ); + const columns = wrapper.find(ReorderableTable).prop('columns'); + expect(columns).toEqual(updatedColumns); + + expect(getUpdatedColumns).toHaveBeenCalledWith({ + columns: requiredParams.columns, + displayedItems: items, + isActivelyEditing: expect.any(Function), + canRemoveLastItem, + isLoading, + lastItemWarning, + uneditableItems, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx new file mode 100644 index 00000000000000..fd7aac957b234c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import classNames from 'classnames'; + +import { useActions, useValues, BindLogic } from 'kea'; + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ReorderableTable } from '../reorderable_table'; + +import { EMPTY_ITEM } from './constants'; +import { getUpdatedColumns } from './get_updated_columns'; +import { InlineEditableTableLogic } from './inline_editable_table_logic'; +import { FormErrors, InlineEditableTableColumn, ItemWithAnID } from './types'; + +import './inline_editable_tables.scss'; + +interface InlineEditableTableProps { + columns: Array>; + items: Item[]; + title: string; + addButtonText?: string; + canRemoveLastItem?: boolean; + className?: string; + description?: React.ReactNode; + isLoading?: boolean; + lastItemWarning?: string; + noItemsMessage?: (editNewItem: () => void) => React.ReactNode; + uneditableItems?: Item[]; +} + +export const InlineEditableTable = ( + props: InlineEditableTableProps & { + instanceId: string; + onAdd(item: Item, onSuccess: () => void): void; + onDelete(item: Item, onSuccess: () => void): void; + onReorder?(items: Item[], oldItems: Item[], onSuccess: () => void): void; + onUpdate(item: Item, onSuccess: () => void): void; + transformItem?(item: Item): Item; + validateItem?(item: Item): FormErrors; + } +) => { + const { + instanceId, + columns, + onAdd, + onDelete, + onReorder, + onUpdate, + transformItem, + validateItem, + ...rest + } = props; + return ( + + + + ); +}; + +export const InlineEditableTableContents = ({ + columns, + items, + title, + addButtonText, + canRemoveLastItem, + className, + description, + isLoading, + lastItemWarning, + noItemsMessage = () => null, + uneditableItems, + ...rest +}: InlineEditableTableProps) => { + const { editingItemId, isEditing, isEditingUnsavedItem } = useValues(InlineEditableTableLogic); + const { editNewItem, reorderItems } = useActions(InlineEditableTableLogic); + + // TODO These two things shoud just be selectors + const isEditingItem = (item: Item) => item.id === editingItemId; + const isActivelyEditing = (item: Item) => isEditing && isEditingItem(item); + + const displayedItems = isEditingUnsavedItem + ? uneditableItems + ? [EMPTY_ITEM, ...items] + : [...items, EMPTY_ITEM] + : items; + + const updatedColumns = getUpdatedColumns({ + columns, + // TODO We shouldn't need this cast here + displayedItems: displayedItems as Item[], + isActivelyEditing, + canRemoveLastItem, + isLoading, + lastItemWarning, + uneditableItems, + }); + + return ( + <> + + + +

{title}

+
+ {!!description && ( + <> + + + {description} + + + )} +
+ + + {addButtonText || + i18n.translate('xpack.enterpriseSearch.inlineEditableTable.newRowButtonLabel', { + defaultMessage: 'New row', + })} + + +
+ + ({ + className: classNames({ + 'is-being-edited': isActivelyEditing(item), + }), + })} + noItemsMessage={noItemsMessage(editNewItem)} + onReorder={reorderItems} + disableDragging={isEditing} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts new file mode 100644 index 00000000000000..f690a38620ecba --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts @@ -0,0 +1,434 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../__mocks__/kea_logic'; + +import { omit } from 'lodash'; + +import { InlineEditableTableLogic } from './inline_editable_table_logic'; + +interface Foo { + id: number; + foo: string; + bar: string; +} + +describe('InlineEditableTableLogic', () => { + const { mount } = new LogicMounter(InlineEditableTableLogic); + + const DEFAULT_VALUES = { + editingItemId: null, + editingItemValue: null, + formErrors: {}, + isEditing: false, + }; + + const SELECTORS = { + doesEditingItemValueContainEmptyProperty: false, + isEditingUnsavedItem: false, + }; + + // Values without selectors + const logicValuesWithoutSelectors = (logic: any) => omit(logic.values, Object.keys(SELECTORS)); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const DEFAULT_LOGIC_PARAMS = { + instanceId: '1', + columns: [ + { + field: 'foo', + render: jest.fn(), + editingRender: jest.fn(), + }, + { + field: 'bar', + render: jest.fn(), + editingRender: jest.fn(), + }, + ], + onAdd: jest.fn(), + onDelete: jest.fn(), + onReorder: jest.fn(), + onUpdate: jest.fn(), + transformItem: jest.fn(), + validateItem: jest.fn(), + }; + + const mountLogic = (values: object = {}, params: object = DEFAULT_LOGIC_PARAMS) => + mount(values, params); + + it('has expected default values', () => { + const logic = mountLogic(); + expect(logic.values).toEqual({ + ...DEFAULT_VALUES, + ...SELECTORS, + }); + }); + + describe('actions', () => { + describe('deleteItem', () => { + const logic = mountLogic(); + logic.actions.deleteItem(); + expect(logicValuesWithoutSelectors(logic)).toEqual(DEFAULT_VALUES); + }); + + describe('doneEditing', () => { + it('resets a bunch of values', () => { + const logic = mountLogic({ + isEditing: true, + editingItemId: 1, + editingItemValue: {}, + formErrors: { foo: 'I am error' }, + }); + logic.actions.doneEditing(); + expect(logicValuesWithoutSelectors(logic)).toEqual(DEFAULT_VALUES); + }); + }); + + describe('editNewItem', () => { + it('updates state to reflect a new item being edited', () => { + const logic = mountLogic({ + isEditing: false, + editingItemId: 1, + editingItemValue: { + id: 1, + foo: 'some foo', + bar: 'some bar', + }, + }); + logic.actions.editNewItem(); + expect(logicValuesWithoutSelectors(logic)).toEqual({ + ...DEFAULT_VALUES, + isEditing: true, + editingItemId: null, + editingItemValue: { + // Note that new values do not yet have an id + foo: '', + bar: '', + }, + }); + }); + }); + + describe('editExistingItem', () => { + it('updates state to reflect the item that was passed being edited', () => { + const logic = mountLogic({ + isEditing: false, + editingItemId: 1, + editingItemValue: { + id: 1, + foo: '', + bar: '', + }, + }); + logic.actions.editExistingItem({ + id: 2, + foo: 'existing foo', + bar: 'existing bar', + }); + expect(logicValuesWithoutSelectors(logic)).toEqual({ + ...DEFAULT_VALUES, + isEditing: true, + editingItemId: 2, + editingItemValue: { + id: 2, + foo: 'existing foo', + bar: 'existing bar', + }, + }); + }); + }); + + describe('setFormErrors', () => { + it('sets formErrors', () => { + const formErrors = { + bar: 'I am an error', + }; + const logic = mountLogic(); + logic.actions.setFormErrors(formErrors); + expect(logicValuesWithoutSelectors(logic)).toEqual({ + ...DEFAULT_VALUES, + formErrors, + }); + }); + }); + + describe('setEditingItemValue', () => { + it('updates the state of the item currently being edited and resets form errors', () => { + const logic = mountLogic({ + editingItemValue: { + id: 1, + foo: '', + bar: '', + }, + formErrors: { foo: 'I am error' }, + }); + logic.actions.setEditingItemValue({ + id: 1, + foo: 'blah blah', + bar: '', + }); + expect(logicValuesWithoutSelectors(logic)).toEqual({ + ...DEFAULT_VALUES, + editingItemValue: { + id: 1, + foo: 'blah blah', + bar: '', + }, + formErrors: {}, + }); + }); + }); + }); + + describe('selectors', () => { + describe('isEditingUnsavedItem', () => { + it('is true when the user is currently editing an unsaved item', () => { + const logic = mountLogic({ + isEditing: true, + editingItemId: null, + }); + + expect(logic.values.isEditingUnsavedItem).toBe(true); + }); + + it('is false when the user is NOT currently editing an unsaved item', () => { + const logic = mountLogic({ + isEditing: true, + editingItemId: 1, + }); + + expect(logic.values.isEditingUnsavedItem).toBe(false); + }); + }); + + describe('doesEditingItemValueContainEmptyProperty', () => { + it('is true when the user is currently editing an item that has empty properties', () => { + const logic = mountLogic({ + isEditing: true, + editingItemValue: { + id: 1, + foo: '', + }, + editingItemId: 1, + }); + + expect(logic.values.doesEditingItemValueContainEmptyProperty).toBe(true); + }); + + it('is false when no properties are empty', () => { + const logic = mountLogic({ + isEditing: true, + editingItemValue: { + id: 1, + foo: 'foo', + }, + editingItemId: 1, + }); + + expect(logic.values.doesEditingItemValueContainEmptyProperty).toBe(false); + }); + + it('is false when the user is not editing anything', () => { + const logic = mountLogic({ + isEditing: true, + editingItemValue: null, + editingItemId: null, + }); + + expect(logic.values.doesEditingItemValueContainEmptyProperty).toBe(false); + }); + }); + }); + + describe('listeners', () => { + describe('reorderItems', () => { + it('will call the provided onReorder callback', () => { + const items: Foo[] = []; + const oldItems: Foo[] = []; + const logic = mountLogic(); + logic.actions.reorderItems(items, oldItems); + expect(DEFAULT_LOGIC_PARAMS.onReorder).toHaveBeenCalledWith( + items, + oldItems, + expect.any(Function) + ); + }); + + it('will not call the onReorder callback if one was not provided', () => { + const items: Foo[] = []; + const oldItems: Foo[] = []; + const logic = mountLogic( + {}, + { + ...DEFAULT_LOGIC_PARAMS, + onReorder: undefined, + } + ); + logic.actions.reorderItems(items, oldItems); + }); + }); + + describe('saveExistingItem', () => { + it('will call the provided onUpdate callback if the item being edited validates', () => { + const editingItemValue = {}; + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue({}); + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + logic.actions.saveExistingItem(); + expect(DEFAULT_LOGIC_PARAMS.onUpdate).toHaveBeenCalledWith( + editingItemValue, + expect.any(Function) + ); + }); + + it('will set form errors and not call the provided onUpdate callback if the item being edited does not validate', () => { + const editingItemValue = {}; + const formErrors = { + foo: 'some error', + }; + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(formErrors); + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + jest.spyOn(logic.actions, 'setFormErrors'); + logic.actions.saveExistingItem(); + expect(DEFAULT_LOGIC_PARAMS.onUpdate).not.toHaveBeenCalled(); + expect(logic.actions.setFormErrors).toHaveBeenCalledWith(formErrors); + }); + + it('will do neither if no value is currently being edited', () => { + const editingItemValue = null; + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + jest.spyOn(logic.actions, 'setFormErrors'); + logic.actions.saveExistingItem(); + expect(DEFAULT_LOGIC_PARAMS.onUpdate).not.toHaveBeenCalled(); + expect(logic.actions.setFormErrors).not.toHaveBeenCalled(); + }); + + it('will always call the provided onUpdate callback if no validateItem param was provided', () => { + const editingItemValue = {}; + const logic = mountLogic( + { + ...DEFAULT_VALUES, + editingItemValue, + }, + { + ...DEFAULT_LOGIC_PARAMS, + validateItem: undefined, + } + ); + logic.actions.saveExistingItem(); + expect(DEFAULT_LOGIC_PARAMS.onUpdate).toHaveBeenCalledWith( + editingItemValue, + expect.any(Function) + ); + }); + }); + + describe('saveNewItem', () => { + it('will call the provided onAdd callback if the new item validates', () => { + const editingItemValue = {}; + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue({}); + const logic = mountLogic( + { + ...DEFAULT_VALUES, + editingItemValue, + }, + { + ...DEFAULT_LOGIC_PARAMS, + transformItem: undefined, + } + ); + logic.actions.saveNewItem(); + expect(DEFAULT_LOGIC_PARAMS.onAdd).toHaveBeenCalledWith( + editingItemValue, + expect.any(Function) + ); + }); + + it('will transform the item first if transformItem callback is provided', () => { + const editingItemValue = {}; + const transformedItem = {}; + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue({}); + DEFAULT_LOGIC_PARAMS.transformItem.mockReturnValue(transformedItem); + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + logic.actions.saveNewItem(); + expect(DEFAULT_LOGIC_PARAMS.onAdd).toHaveBeenCalledWith( + transformedItem, + expect.any(Function) + ); + }); + + it('will set form errors and not call the provided onAdd callback if the item being edited does not validate', () => { + const editingItemValue = {}; + const formErrors = { + foo: 'some error', + }; + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(formErrors); + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + jest.spyOn(logic.actions, 'setFormErrors'); + logic.actions.saveNewItem(); + expect(DEFAULT_LOGIC_PARAMS.onAdd).not.toHaveBeenCalled(); + expect(logic.actions.setFormErrors).toHaveBeenCalledWith(formErrors); + }); + + it('will do nothing if no value is currently being edited', () => { + const editingItemValue = null; + const logic = mountLogic({ + ...DEFAULT_VALUES, + editingItemValue, + }); + jest.spyOn(logic.actions, 'setFormErrors'); + logic.actions.saveNewItem(); + expect(DEFAULT_LOGIC_PARAMS.onAdd).not.toHaveBeenCalled(); + expect(logic.actions.setFormErrors).not.toHaveBeenCalled(); + }); + + it('will always call the provided onAdd callback if no validateItem param was provided', () => { + const editingItemValue = {}; + const logic = mountLogic( + { + ...DEFAULT_VALUES, + editingItemValue, + }, + { + ...DEFAULT_LOGIC_PARAMS, + validateItem: undefined, + transformItem: undefined, + } + ); + logic.actions.saveNewItem(); + expect(DEFAULT_LOGIC_PARAMS.onAdd).toHaveBeenCalledWith( + editingItemValue, + expect.any(Function) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts new file mode 100644 index 00000000000000..ab6694861a6ad6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { FormErrors, InlineEditableTableColumn, ItemWithAnID } from './types'; + +interface InlineEditableTableActions { + deleteItem(item: Item): { item: Item }; + doneEditing(): void; + editNewItem(): void; + editExistingItem(item: Item): { item: Item }; + reorderItems(items: Item[], oldItems: Item[]): { items: Item[]; oldItems: Item[] }; + saveExistingItem(): void; + saveNewItem(): void; + setEditingItemValue(newValue: Item): { item: Item }; + setFormErrors(formErrors: FormErrors): { formErrors: FormErrors }; +} + +const generateEmptyItem = ( + columns: Array> +): Item => { + const emptyItem = columns.reduce((acc, column) => ({ ...acc, [column.field]: '' }), {}) as Item; + return emptyItem; +}; + +const getUnsavedItemId = () => null; +const doesIdMatchUnsavedId = (idToCheck: number) => idToCheck === getUnsavedItemId(); + +interface InlineEditableTableValues { + // TODO This could likely be a selector + isEditing: boolean; + // TODO we should editingItemValue have editingItemValue and editingItemId should be a selector + editingItemId: Item['id'] | null; // editingItem is null when the user is editing a new but not saved item + editingItemValue: Item | null; + formErrors: FormErrors; + isEditingUnsavedItem: boolean; + doesEditingItemValueContainEmptyProperty: boolean; +} + +interface InlineEditableTableProps { + columns: Array>; + instanceId: string; + // TODO Because these callbacks are params, they are only set on the logic once (i.e., they are cached) + // which makes using "useState" to back this really hard. + onAdd(item: Item, onSuccess: () => void): void; + onDelete(item: Item, onSuccess: () => void): void; + onReorder?(items: Item[], oldItems: Item[], onSuccess: () => void): void; + onUpdate(item: Item, onSuccess: () => void): void; + transformItem?(item: Item): Item; + validateItem?(item: Item): FormErrors; +} + +type InlineEditableTableLogicType = MakeLogicType< + InlineEditableTableValues, + InlineEditableTableActions, + InlineEditableTableProps +>; + +export const InlineEditableTableLogic = kea>({ + path: (key: string) => ['enterprise_search', 'inline_editable_table_logic', key], + key: (props) => props.instanceId, + actions: () => ({ + deleteItem: (item) => ({ item }), + doneEditing: true, + editNewItem: true, + editExistingItem: (item) => ({ item }), + reorderItems: (items, oldItems) => ({ items, oldItems }), + saveExistingItem: true, + saveNewItem: true, + setEditingItemValue: (newValue) => ({ item: newValue }), + setFormErrors: (formErrors) => ({ formErrors }), + }), + reducers: ({ props: { columns } }) => ({ + isEditing: [ + false, + { + doneEditing: () => false, + editNewItem: () => true, + editExistingItem: () => true, + }, + ], + editingItemId: [ + null, + { + doneEditing: () => null, + editNewItem: () => getUnsavedItemId(), + editExistingItem: (_, { item }) => item.id, + }, + ], + editingItemValue: [ + null, + { + doneEditing: () => null, + editNewItem: () => generateEmptyItem(columns), + editExistingItem: (_, { item }) => item, + setEditingItemValue: (_, { item }) => item, + }, + ], + formErrors: [ + {}, + { + doneEditing: () => ({}), + setEditingItemValue: () => ({}), + setFormErrors: (_, { formErrors }) => formErrors, + }, + ], + }), + selectors: ({ selectors }) => ({ + isEditingUnsavedItem: [ + () => [selectors.isEditing, selectors.editingItemId], + (isEditing, editingItemId) => { + return isEditing && doesIdMatchUnsavedId(editingItemId); + }, + ], + doesEditingItemValueContainEmptyProperty: [ + () => [selectors.editingItemValue], + (editingItemValue: object) => { + return ( + Object.values(editingItemValue || {}).findIndex( + (value) => typeof value === 'string' && value.length === 0 + ) > -1 + ); + }, + ], + }), + listeners: ({ + values, + actions, + props: { onAdd, onDelete, onReorder, onUpdate, transformItem, validateItem }, + }) => ({ + saveNewItem: () => { + if (!values.editingItemValue) return; + + const itemToSave = transformItem + ? transformItem(values.editingItemValue) + : values.editingItemValue; + const errors: FormErrors = + typeof validateItem === 'undefined' ? {} : validateItem(itemToSave); + if (Object.keys(errors).length) { + actions.setFormErrors(errors); + } else { + onAdd(itemToSave, actions.doneEditing); + } + }, + deleteItem: ({ item: itemToDelete }) => { + onDelete(itemToDelete, actions.doneEditing); + }, + reorderItems: ({ items, oldItems }) => { + if (onReorder) onReorder(items, oldItems, actions.doneEditing); + }, + saveExistingItem: () => { + if (!values.editingItemValue) return; + const itemToSave = values.editingItemValue; + const errors: FormErrors = + typeof validateItem === 'undefined' ? {} : validateItem(itemToSave); + if (Object.keys(errors).length) { + actions.setFormErrors(errors); + } else { + onUpdate(itemToSave, actions.doneEditing); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_tables.scss b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_tables.scss new file mode 100644 index 00000000000000..5a000dcc4899e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_tables.scss @@ -0,0 +1,3 @@ +.inlineEditableTable__descriptionText { + max-width: 60rem; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/types.ts new file mode 100644 index 00000000000000..35e60cdf4e1469 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Column } from '../reorderable_table/types'; + +export interface FormErrors { + [key: string]: string | undefined; +} + +export type ItemWithAnID = { + id: number | null; +} & object; + +export interface EditingRenderFlags { + isInvalid: boolean; + isLoading: boolean; +} + +export interface InlineEditableTableColumn extends Column { + field: string; + editingRender: ( + item: Item, + onChange: (value: string) => void, + flags: EditingRenderFlags + ) => React.ReactNode; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts index 77c1495977d2f4..1560906b5c8aab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts @@ -11,6 +11,6 @@ export interface DraggableUXStyles { flexGrow?: number; } export interface Column extends DraggableUXStyles { - name: string; + name?: string; render: (item: Item) => React.ReactNode; } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index a3bc2ed082b1af..70e1ac25a7eccd 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -75,7 +75,6 @@ export class ImportCompleteView extends Component { {}} options={{ readOnly: true, lineNumbers: 'off', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index 1edf1bf6972511..d2342dafca2eb1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -295,7 +295,7 @@ const AgentDetailsPageContent: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; }> = ({ agent, agentPolicy }) => { - useBreadcrumbs('agent_list', { + useBreadcrumbs('agent_details', { agentHost: typeof agent.local_metadata.host === 'object' && typeof agent.local_metadata.host.hostname === 'string' diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index 8c637006fb0cd4..568aafddecbadb 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -35,9 +35,6 @@ export const getRegistryUrl = (): string => { const isEnterprise = licenseService.isEnterprise(); if (customUrl && isEnterprise) { - appContextService - .getLogger() - .info('Custom registry url is an experimental feature and is unsupported.'); return customUrl; } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index 076bee7d0e7a48..733d962a86e9eb 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -59,6 +59,13 @@ export async function startFleetServerSetup() { try { // We need licence to be initialized before using the SO service. await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); + + const customUrl = appContextService.getConfig()?.registryUrl; + const isEnterprise = licenseService.isEnterprise(); + if (customUrl && isEnterprise) { + logger.info('Custom registry url is an experimental feature and is unsupported.'); + } + await runFleetServerMigration(); _isFleetServerSetup = true; } catch (err) { diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 4e653393100c90..e13cd8a0adba16 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -7,5 +7,9 @@ "requiredPlugins": ["licensing", "data", "navigation", "savedObjects", "kibanaLegacy"], "optionalPlugins": ["home", "features"], "configPath": ["xpack", "graph"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] + "requiredBundles": ["kibanaUtils", "kibanaReact", "home"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts index 6d05f3d63f5773..798b74e40055fd 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts @@ -15,39 +15,65 @@ const createSetPrimaryShardSizeAction = (testBed: TestBed) => async ( units?: string ) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxPrimaryShardSize').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxPrimaryShardSize.select').simulate('change', { - target: { value: units }, - }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxPrimaryShardSize.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxPrimaryShardSize.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; const createSetMaxAgeAction = (testBed: TestBed) => async (value: string, units?: string) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxAge').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxAgeUnits.select').simulate('change', { target: { value: units } }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxAgeUnits.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxAgeUnits.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; const createSetMaxSizeAction = (testBed: TestBed) => async (value: string, units?: string) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxSizeStored').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxSizeStoredUnits.select').simulate('change', { - target: { value: units }, - }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxSizeStoredUnits.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxSizeStoredUnits.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; export const createRolloverActions = (testBed: TestBed) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx index 2d3704e252ac8c..7fbdaf344b8fae 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx @@ -9,16 +9,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NumericField, SelectField } from '../../../../../../../shared_imports'; +import { NumericField } from '../../../../../../../shared_imports'; import { UseField } from '../../../../form'; import { ROLLOVER_FORM_PATHS } from '../../../../constants'; +import { UnitField } from './unit_field'; import { maxAgeUnits } from '../constants'; export const MaxAgeField: FunctionComponent = () => { return ( - - + + { euiFieldProps: { 'data-test-subj': `hot-selectedMaxAge`, min: 1, - }, - }} - /> - - - ), - options: maxAgeUnits, }, }} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx index c6f8abef3f2c2f..e847d773e2a88e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx @@ -14,8 +14,8 @@ import { ROLLOVER_FORM_PATHS } from '../../../../constants'; export const MaxDocumentCountField: FunctionComponent = () => { return ( - - + + { return ( - - + + { content={i18nTexts.deprecationMessage} /> ), - min: 1, - }, - }} - /> - - - + ), }, }} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx index eed23bd48a0fa1..d9c7ed5a24b99c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx @@ -9,16 +9,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NumericField, SelectField } from '../../../../../../../shared_imports'; +import { NumericField } from '../../../../../../../shared_imports'; import { UseField } from '../../../../form'; import { ROLLOVER_FORM_PATHS } from '../../../../constants'; +import { UnitField } from './unit_field'; import { maxSizeStoredUnits } from '../constants'; export const MaxPrimaryShardSizeField: FunctionComponent = () => { return ( - - + + { euiFieldProps: { 'data-test-subj': 'hot-selectedMaxPrimaryShardSize', min: 1, - }, - }} - /> - - - ), }, }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx new file mode 100644 index 00000000000000..2ef8917d539892 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { EuiFilterSelectItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui'; + +import { UseField } from '../../../../form'; + +interface Props { + path: string; + euiFieldProps?: Record; + options: Array<{ + value: string; + text: string; + }>; +} + +export const UnitField: FunctionComponent = ({ path, options, euiFieldProps }) => { + const [open, setOpen] = useState(false); + + return ( + + {(field) => { + const onSelect = (option: string) => { + field.setValue(option); + setOpen(false); + }; + + return ( + setOpen((x) => !x)} + data-test-subj="show-filters-button" + > + {options.find((x) => x.value === field.value)?.text} + + } + ownFocus + panelPaddingSize="none" + isOpen={open} + closePopover={() => setOpen(false)} + {...euiFieldProps} + > + {options.map((item) => ( + onSelect(item.value)} + data-test-subj={`filter-option-${item.value}`} + > + {item.text} + + ))} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 3c30c6d3a678fc..d6a36b99c20aa8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -135,6 +135,7 @@ export const HotPhase: FunctionComponent = () => { {showEmptyRolloverFieldsError && ( <> { +export function createMetricThresholdAlertType(): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { @@ -44,5 +44,6 @@ Reason: } ), requiresAppContext: false, + format: formatReason, }; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts new file mode 100644 index 00000000000000..7a0140ab05652a --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; + +export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; + const link = '/app/metrics/explorer'; // TODO https://github.com/elastic/kibana/issues/106497 & https://github.com/elastic/kibana/issues/106958 + + return { + reason, + link, + }; +}; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3c22c1ad7a76dd..76e3e777e63788 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -41,8 +41,9 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.observability.observabilityRuleTypeRegistry.register( createLogThresholdAlertType() ); - pluginsSetup.triggersActionsUi.ruleTypeRegistry.register(createMetricThresholdAlertType()); - + pluginsSetup.observability.observabilityRuleTypeRegistry.register( + createMetricThresholdAlertType() + ); pluginsSetup.observability.dashboard.register({ appName: 'infra_logs', hasData: getLogsHasDataFetcher(core.getStartServices), diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 0ec071b97d7cf6..f33bcd2fcab0ca 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericParams, SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { Lifecycle } from '@hapi/hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { JsonArray, JsonValue } from '@kbn/common-utils'; @@ -38,7 +38,7 @@ export interface InfraServerPluginStartDeps { data: DataPluginStart; } -export interface CallWithRequestParams extends GenericParams { +export interface CallWithRequestParams extends estypes.RequestBase { max_concurrent_shard_requests?: number; name?: string; index?: string | string[]; @@ -50,6 +50,7 @@ export interface CallWithRequestParams extends GenericParams { path?: string; query?: string | object; track_total_hits?: boolean | number; + body?: any; } export type InfraResponse = Lifecycle.ReturnValue; @@ -117,7 +118,7 @@ export interface InfraDatabaseGetIndicesResponse { }; } -export type SearchHit = SearchResponse['hits']['hits'][0]; +export type SearchHit = estypes.SearchHit; export interface SortedSearchHit extends SearchHit { sort: any[]; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index fd1dc43c191fbc..18de1a2ad5c007 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,6 @@ */ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { Comparator } from './types'; import * as mocks from './test_mocks'; // import { RecoveredActionGroup } from '../../../../../alerting/common'; import { @@ -14,10 +13,19 @@ import { AlertServicesMock, AlertInstanceMock, } from '../../../../../alerting/server/mocks'; +import { LifecycleAlertServices } from '../../../../../rule_registry/server'; +import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks'; +import { createLifecycleRuleExecutorMock } from '../../../../../rule_registry/server/utils/create_lifecycle_rule_executor_mock'; import { InfraSources } from '../../sources'; -import { MetricThresholdAlertExecutorOptions } from './register_metric_threshold_alert_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { AlertInstanceContext, AlertInstanceState } from '../../../../../alerting/server'; +import { + Aggregators, + Comparator, + CountMetricExpressionParams, + NonCountMetricExpressionParams, +} from './types'; interface AlertTestInstance { instance: AlertInstanceMock; @@ -27,11 +35,33 @@ interface AlertTestInstance { let persistAlertInstances = false; // eslint-disable-line prefer-const +type TestRuleState = Record & { + aRuleStateKey: string; +}; + +const initialRuleState: TestRuleState = { + aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', +}; + const mockOptions = { alertId: '', startedAt: new Date(), previousStartedAt: null, - state: {}, + state: { + wrapped: initialRuleState, + trackedAlerts: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + }, + TEST_ALERT_1: { + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + started: '2020-01-02T12:00:00.000Z', + }, + }, + }, spaceId: '', name: '', tags: [], @@ -62,22 +92,20 @@ describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => - executor(({ + executor({ + ...mockOptions, services, params: { sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, }, ], }, - /** - * TODO: Remove this use of `as` by utilizing a proper type - */ - } as unknown) as MetricThresholdAlertExecutorOptions); + }); test('alerts as expected with the > comparator', async () => { await execute(Comparator.GT, [0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); @@ -129,7 +157,7 @@ describe('The metric threshold alert type', () => { }); describe('querying with a groupBy parameter', () => { - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, @@ -137,7 +165,7 @@ describe('The metric threshold alert type', () => { groupBy: 'something', criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, }, @@ -173,21 +201,23 @@ describe('The metric threshold alert type', () => { comparator: Comparator, thresholdA: number[], thresholdB: number[], - groupBy: string = '' + groupBy: string = '', + sourceId: string = 'default' ) => executor({ ...mockOptions, services, params: { + sourceId, groupBy, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold: thresholdA, }, { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold: thresholdB, metric: 'test.metric.2', @@ -228,19 +258,18 @@ describe('The metric threshold alert type', () => { }); describe('querying with the count aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseCountCriterion, comparator, threshold, - aggType: 'count', - metric: undefined, - }, + } as CountMetricExpressionParams, ], }, }); @@ -253,17 +282,17 @@ describe('The metric threshold alert type', () => { }); describe('querying with the p99 aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, - aggType: 'p99', + aggType: Aggregators.P99, metric: 'test.metric.2', }, ], @@ -278,17 +307,18 @@ describe('The metric threshold alert type', () => { }); describe('querying with the p95 aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, - aggType: 'p95', + aggType: Aggregators.P95, metric: 'test.metric.1', }, ], @@ -303,16 +333,17 @@ describe('The metric threshold alert type', () => { }); describe("querying a metric that hasn't reported data", () => { const instanceID = '*'; - const execute = (alertOnNoData: boolean) => + const execute = (alertOnNoData: boolean, sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, - threshold: 1, + threshold: [1], metric: 'test.metric.3', }, ], @@ -331,18 +362,18 @@ describe('The metric threshold alert type', () => { describe("querying a rate-aggregated metric that hasn't reported data", () => { const instanceID = '*'; - const execute = () => + const execute = (sourceId: string = 'default') => executor({ ...mockOptions, services, params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, - threshold: 1, + threshold: [1], metric: 'test.metric.3', - aggType: 'rate', + aggType: Aggregators.RATE, }, ], alertOnNoData: true, @@ -370,7 +401,7 @@ describe('The metric threshold alert type', () => { params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, threshold, }, @@ -417,7 +448,7 @@ describe('The metric threshold alert type', () => { sourceId: 'default', criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, metric: 'test.metric.pct', comparator: Comparator.GT, threshold: [0.75], @@ -450,11 +481,19 @@ const mockLibs: any = { config: createMockStaticConfiguration({}), }), configuration: createMockStaticConfiguration({}), + metricsRules: { + createLifecycleRuleExecutor: createLifecycleRuleExecutorMock, + }, }; const executor = createMetricThresholdExecutor(mockLibs); -const services: AlertServicesMock = alertsMock.createAlertServices(); +const alertsServices = alertsMock.createAlertServices(); +const services: AlertServicesMock & + LifecycleAlertServices = { + ...alertsServices, + ...ruleRegistryMocks.createLifecycleAlertServices(alertsServices), +}; services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from); @@ -527,9 +566,18 @@ function mostRecentAction(id: string) { return alertInstances.get(id)!.actionQueue.pop(); } -const baseCriterion = { - aggType: 'avg', +const baseNonCountCriterion: Pick< + NonCountMetricExpressionParams, + 'aggType' | 'metric' | 'timeSize' | 'timeUnit' +> = { + aggType: Aggregators.AVERAGE, metric: 'test.metric.1', timeSize: 1, timeUnit: 'm', }; + +const baseCountCriterion: Pick = { + aggType: Aggregators.COUNT, + timeSize: 1, + timeUnit: 'm', +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 190d8e028fe0d2..259318b6c93a1a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -8,7 +8,14 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { RecoveredActionGroup } from '../../../../../alerting/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { + ActionGroupIdsOf, + RecoveredActionGroup, + AlertInstanceState, + AlertInstanceContext, +} from '../../../../../alerting/common'; +import { AlertTypeState, AlertInstance } from '../../../../../alerting/server'; import { InfraBackendLibs } from '../../infra_types'; import { buildErrorAlertReason, @@ -20,18 +27,48 @@ import { import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; -import { - MetricThresholdAlertExecutorOptions, - MetricThresholdAlertType, -} from './register_metric_threshold_alert_type'; -export const createMetricThresholdExecutor = ( - libs: InfraBackendLibs -): MetricThresholdAlertType['executor'] => - async function (options: MetricThresholdAlertExecutorOptions) { +export type MetricThresholdAlertTypeParams = Record; +export type MetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used +export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used + +type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS +>; + +type MetricThresholdAlertInstance = AlertInstance< + MetricThresholdAlertInstanceState, + MetricThresholdAlertInstanceContext, + MetricThresholdAllowedActionGroups +>; + +type MetricThresholdAlertInstanceFactory = ( + id: string, + reason: string, + threshold?: number | undefined, + value?: number | undefined +) => MetricThresholdAlertInstance; + +export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => + libs.metricsRules.createLifecycleRuleExecutor< + MetricThresholdAlertTypeParams, + MetricThresholdAlertTypeState, + MetricThresholdAlertInstanceState, + MetricThresholdAlertInstanceContext, + MetricThresholdAllowedActionGroups + >(async function (options) { const { services, params } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const { alertWithLifecycle, savedObjectsClient } = services; + const alertInstanceFactory: MetricThresholdAlertInstanceFactory = (id, reason) => + alertWithLifecycle({ + id, + fields: { + [ALERT_REASON]: reason, + }, + }); const { sourceId, alertOnNoData } = params as { sourceId?: string; @@ -39,7 +76,7 @@ export const createMetricThresholdExecutor = ( }; const source = await libs.sources.getSourceConfiguration( - services.savedObjectsClient, + savedObjectsClient, sourceId || 'default' ); const config = source.configuration; @@ -114,8 +151,7 @@ export const createMetricThresholdExecutor = ( : nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; - const alertInstance = services.alertInstanceFactory(`${group}`); - + const alertInstance = alertInstanceFactory(`${group}`, reason); alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], @@ -133,7 +169,7 @@ export const createMetricThresholdExecutor = ( }); } } - }; + }); export const FIRED_ACTIONS = { id: 'metrics.threshold.fired', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 9418762d3e1bfd..054585e541ba10 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -7,13 +7,8 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { - AlertType, - AlertInstanceState, - AlertInstanceContext, - AlertExecutorOptions, - ActionGroupIdsOf, -} from '../../../../../alerting/server'; +import { ActionGroupIdsOf } from '../../../../../alerting/common'; +import { AlertType, PluginSetupContract } from '../../../../../alerting/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, @@ -33,29 +28,17 @@ import { thresholdActionVariableDescription, } from '../common/messages'; -export type MetricThresholdAlertType = AlertType< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - never, // Only use if defining useSavedObjectReferences hook - Record, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf ->; -export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - Record, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf +type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; +export type MetricThresholdAlertType = Omit & { + ActionGroupIdsOf: MetricThresholdAllowedActionGroups; +}; -export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { +export async function registerMetricThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), @@ -77,7 +60,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric metric: schema.never(), }); - return { + alertingPlugin.registerType({ id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.alertName', { defaultMessage: 'Metric threshold', @@ -115,5 +98,5 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric ], }, producer: 'infrastructure', - }; + }); } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 37f21022f183db..101be1f77b9d0a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -33,12 +33,12 @@ interface BaseMetricExpressionParams { warningThreshold?: number[]; } -interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { +export interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Exclude; metric: string; } -interface CountMetricExpressionParams extends BaseMetricExpressionParams { +export interface CountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Aggregators.COUNT; metric: never; } diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index d7df2afd8038b8..d0af9ac4ce6695 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -20,10 +20,13 @@ const registerAlertTypes = ( ml?: MlPluginSetup ) => { if (alertingPlugin) { - alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); - const registerFns = [registerLogThresholdAlertType, registerMetricInventoryThresholdAlertType]; + const registerFns = [ + registerLogThresholdAlertType, + registerMetricInventoryThresholdAlertType, + registerMetricThresholdAlertType, + ]; registerFns.forEach((fn) => { fn(alertingPlugin, libs); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 54bb3efd75b392..c17eeec91dc2b6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -340,9 +340,10 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -406,9 +407,10 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -569,9 +571,10 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -635,9 +638,10 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -1240,9 +1244,10 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -1310,9 +1315,10 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -1477,9 +1483,10 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], @@ -1547,9 +1554,10 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = displayValueSettings={ Object { "isAlternatingValueLabel": false, - "isValueContainedInElement": true, + "isValueContainedInElement": false, "overflowConstraints": Array [ "chartEdges", + "barGeometry", ], "showValueLabel": false, "valueFormatter": [Function], diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 56867c625bb6fa..8eb7ec1e0f623e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -167,7 +167,7 @@ export const getXyChartRenderer = (dependencies: { }); function getValueLabelsStyling(isHorizontal: boolean) { - const VALUE_LABELS_MAX_FONTSIZE = 15; + const VALUE_LABELS_MAX_FONTSIZE = 12; const VALUE_LABELS_MIN_FONTSIZE = 10; const VALUE_LABELS_VERTICAL_OFFSET = -10; const VALUE_LABELS_HORIZONTAL_OFFSET = 10; @@ -175,7 +175,7 @@ function getValueLabelsStyling(isHorizontal: boolean) { return { displayValue: { fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, - fill: { textInverted: true, textBorder: 2 }, + fill: { textContrast: true, textInverted: false, textBorder: 0 }, alignment: isHorizontal ? { vertical: VerticalAlignment.Middle, @@ -792,9 +792,12 @@ export function XYChart({ // * in some scenarios value labels are not strings, and this breaks the elastic-chart lib valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '', showValueLabel: shouldShowValueLabels && valueLabels !== 'hide', + isValueContainedInElement: false, isAlternatingValueLabel: false, - isValueContainedInElement: true, - overflowConstraints: [LabelOverflowConstraint.ChartEdges], + overflowConstraints: [ + LabelOverflowConstraint.ChartEdges, + LabelOverflowConstraint.BarGeometry, + ], }, }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 69ec0740948fcb..f0cec4abf0b14d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import { Feature } from 'geojson'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson, @@ -274,7 +274,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle const requestId: string = afterKey ? `${this.getId()} afterKey ${afterKey.geoSplit}` : this.getId(); - const esResponse: SearchResponse = await this._runEsQuery({ + const esResponse: estypes.SearchResponse = await this._runEsQuery({ requestId, requestName: `${layerName} (${requestCount})`, searchSource, @@ -291,8 +291,10 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle features.push(...convertCompositeRespToGeoJson(esResponse, this._descriptor.requestType)); - afterKey = esResponse.aggregations.compositeSplit.after_key; - if (esResponse.aggregations.compositeSplit.buckets.length < gridsPerRequest) { + const aggr = esResponse.aggregations + ?.compositeSplit as estypes.AggregationsCompositeBucketAggregate; + afterKey = aggr.after_key; + if (aggr.buckets.length < gridsPerRequest) { // Finished because request did not get full resultset back break; } diff --git a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx index fadf4c5872507a..827ce958deb119 100644 --- a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx +++ b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx @@ -134,7 +134,10 @@ export const EnableAlertsModal: React.FC = ({ alerts }: Props) => { - + - this is the completed report data + this is the failed report error

diff --git a/x-pack/plugins/reporting/public/lib/job.ts b/x-pack/plugins/reporting/public/lib/job.ts deleted file mode 100644 index c882e8b92986bc..00000000000000 --- a/x-pack/plugins/reporting/public/lib/job.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JobId, ReportApiJSON, ReportSource, TaskRunResult } from '../../common/types'; - -type ReportPayload = ReportSource['payload']; - -/* - * This class represents a report job for the UI - * It can be instantiated with ReportApiJSON: the response data format for the report job APIs - */ -export class Job { - public id: JobId; - public index: string; - - public objectType: ReportPayload['objectType']; - public title: ReportPayload['title']; - public isDeprecated: ReportPayload['isDeprecated']; - public browserTimezone?: ReportPayload['browserTimezone']; - public layout: ReportPayload['layout']; - - public jobtype: ReportSource['jobtype']; - public created_by: ReportSource['created_by']; - public created_at: ReportSource['created_at']; - public started_at: ReportSource['started_at']; - public completed_at: ReportSource['completed_at']; - public status: ReportSource['status']; - public attempts: ReportSource['attempts']; - public max_attempts: ReportSource['max_attempts']; - - public timeout: ReportSource['timeout']; - public kibana_name: ReportSource['kibana_name']; - public kibana_id: ReportSource['kibana_id']; - public browser_type: ReportSource['browser_type']; - - public size?: TaskRunResult['size']; - public content_type?: TaskRunResult['content_type']; - public csv_contains_formulas?: TaskRunResult['csv_contains_formulas']; - public max_size_reached?: TaskRunResult['max_size_reached']; - public warnings?: TaskRunResult['warnings']; - - constructor(report: ReportApiJSON) { - this.id = report.id; - this.index = report.index; - - this.jobtype = report.jobtype; - this.objectType = report.payload.objectType; - this.title = report.payload.title; - this.layout = report.payload.layout; - this.created_by = report.created_by; - this.created_at = report.created_at; - this.started_at = report.started_at; - this.completed_at = report.completed_at; - this.status = report.status; - this.attempts = report.attempts; - this.max_attempts = report.max_attempts; - - this.timeout = report.timeout; - this.kibana_name = report.kibana_name; - this.kibana_id = report.kibana_id; - this.browser_type = report.browser_type; - this.browserTimezone = report.payload.browserTimezone; - this.size = report.output?.size; - this.content_type = report.output?.content_type; - - this.isDeprecated = report.payload.isDeprecated || false; - this.csv_contains_formulas = report.output?.csv_contains_formulas; - this.max_size_reached = report.output?.max_size_reached; - this.warnings = report.output?.warnings; - } -} diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx new file mode 100644 index 00000000000000..96967dc9226c90 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import React from 'react'; +import { JOB_STATUSES } from '../../common/constants'; +import { JobId, ReportApiJSON, ReportSource, TaskRunResult } from '../../common/types'; + +const { COMPLETED, FAILED, PENDING, PROCESSING, WARNINGS } = JOB_STATUSES; + +type ReportPayload = ReportSource['payload']; + +/* + * This class represents a report job for the UI + * It can be instantiated with ReportApiJSON: the response data format for the report job APIs + */ +export class Job { + public id: JobId; + public index: string; + + public objectType: ReportPayload['objectType']; + public title: ReportPayload['title']; + public isDeprecated: ReportPayload['isDeprecated']; + public browserTimezone?: ReportPayload['browserTimezone']; + public layout: ReportPayload['layout']; + + public jobtype: ReportSource['jobtype']; + public created_by: ReportSource['created_by']; + public created_at: ReportSource['created_at']; + public started_at: ReportSource['started_at']; + public completed_at: ReportSource['completed_at']; + public status: JOB_STATUSES; // FIXME: can not use ReportSource['status'] due to type mismatch + public attempts: ReportSource['attempts']; + public max_attempts: ReportSource['max_attempts']; + + public timeout: ReportSource['timeout']; + public kibana_name: ReportSource['kibana_name']; + public kibana_id: ReportSource['kibana_id']; + public browser_type: ReportSource['browser_type']; + + public size?: TaskRunResult['size']; + public content_type?: TaskRunResult['content_type']; + public csv_contains_formulas?: TaskRunResult['csv_contains_formulas']; + public max_size_reached?: TaskRunResult['max_size_reached']; + public warnings?: TaskRunResult['warnings']; + + constructor(report: ReportApiJSON) { + this.id = report.id; + this.index = report.index; + + this.jobtype = report.jobtype; + this.objectType = report.payload.objectType; + this.title = report.payload.title; + this.layout = report.payload.layout; + this.created_by = report.created_by; + this.created_at = report.created_at; + this.started_at = report.started_at; + this.completed_at = report.completed_at; + this.status = report.status as JOB_STATUSES; + this.attempts = report.attempts; + this.max_attempts = report.max_attempts; + + this.timeout = report.timeout; + this.kibana_name = report.kibana_name; + this.kibana_id = report.kibana_id; + this.browser_type = report.browser_type; + this.browserTimezone = report.payload.browserTimezone; + this.size = report.output?.size; + this.content_type = report.output?.content_type; + + this.isDeprecated = report.payload.isDeprecated || false; + this.csv_contains_formulas = report.output?.csv_contains_formulas; + this.max_size_reached = report.output?.max_size_reached; + this.warnings = report.output?.warnings; + } + + getStatusMessage() { + const status = this.status; + let smallMessage; + if (status === PENDING) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.pendingStatusReachedText', { + defaultMessage: 'Waiting for job to be processed.', + }); + } else if (status === PROCESSING) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.attemptXofY', { + defaultMessage: 'Attempt {attempts} of {max_attempts}.', + values: { attempts: this.attempts, max_attempts: this.max_attempts }, + }); + } else if (this.getWarnings()) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.warningsText', { + defaultMessage: 'See report info for warnings.', + }); + } else if (this.getError()) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.errorText', { + defaultMessage: 'See report info for error details.', + }); + } + + if (smallMessage) { + return ( + + {smallMessage} + + ); + } + + return null; + } + + getStatus() { + const statusLabel = jobStatusLabelsMap.get(this.status) as string; + const statusTimestamp = this.getStatusTimestamp(); + + if (statusTimestamp) { + return ( + {this.formatDate(statusTimestamp)} + ), + }} + /> + ); + } + + return statusLabel; + } + + getStatusLabel() { + return ( + <> + {this.getStatus()} + {this.getStatusMessage()} + + ); + } + + getCreatedAtLabel() { + if (this.created_by) { + return ( + <> +

{this.formatDate(this.created_at)}
+ {this.created_by} + + ); + } + return this.formatDate(this.created_at); + } + + /* + * We use `output.warnings` to show the error of a failed report job, + * and to show warnings of a job that completed with warnings. + */ + + // There is no error unless the status is 'failed' + getError() { + if (this.status === FAILED) { + return this.warnings; + } + } + + getWarnings() { + if (this.status !== FAILED) { + const warnings: string[] = []; + if (this.isDeprecated) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { + defaultMessage: + 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', + }) + ); + } + if (this.csv_contains_formulas) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { + defaultMessage: + 'Your CSV contains characters which spreadsheet applications can interpret as formulas.', + }) + ); + } + if (this.max_size_reached) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { + defaultMessage: 'Max size reached, contains partial data.', + }) + ); + } + + if (this.warnings?.length) { + warnings.push(...this.warnings); + } + + if (warnings.length) { + return ( +
    + {warnings.map((w, i) => { + return
  • {w}
  • ; + })} +
+ ); + } + } + } + + private formatDate(timestamp: string) { + try { + return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); + } catch (error) { + // ignore parse error and display unformatted value + return timestamp; + } + } + + private getStatusTimestamp() { + const status = this.status; + if (status === PROCESSING && this.started_at) { + return this.started_at; + } + + if (this.completed_at && ([COMPLETED, FAILED, WARNINGS] as string[]).includes(status)) { + return this.completed_at; + } + + return this.created_at; + } +} + +const jobStatusLabelsMap = new Map([ + [ + PENDING, + i18n.translate('xpack.reporting.jobStatuses.pendingText', { + defaultMessage: 'Pending', + }), + ], + [ + PROCESSING, + i18n.translate('xpack.reporting.jobStatuses.processingText', { + defaultMessage: 'Processing', + }), + ], + [ + COMPLETED, + i18n.translate('xpack.reporting.jobStatuses.completedText', { + defaultMessage: 'Completed', // NOTE: a job is `completed` not `completed_with_warings` if it has reached max size or possibly contains csv characters + }), + ], + [ + WARNINGS, + i18n.translate('xpack.reporting.jobStatuses.warningText', { + defaultMessage: 'Completed', + }), + ], + [ + FAILED, + i18n.translate('xpack.reporting.jobStatuses.failedText', { + defaultMessage: 'Failed', + }), + ], +]); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 90411884332c83..9396f232774960 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; @@ -19,11 +20,6 @@ import { DownloadReportFn, JobId, ManagementLinkFn, ReportApiJSON } from '../../ import { add } from '../../notifier/job_completion_notifications'; import { Job } from '../job'; -export interface JobContent { - content: string; - content_type: boolean; -} - export interface DiagnoseResponse { help: string[]; success: boolean; @@ -46,7 +42,7 @@ interface IReportingAPI { deleteReport(jobId: string): Promise; list(page: number, jobIds: string[]): Promise; // gets the first 10 report of the page total(): Promise; - getError(jobId: string): Promise; + getError(jobId: string): Promise; getInfo(jobId: string): Promise; findForJobIds(jobIds: string[]): Promise; @@ -108,8 +104,16 @@ export class ReportingAPIClient implements IReportingAPI { } public async getError(jobId: string) { - return await this.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, + const job = await this.getInfo(jobId); + + if (job.warnings?.[0]) { + // the error message of a failed report is a singular string in the warnings array + return job.warnings[0]; + } + + return i18n.translate('xpack.reporting.apiClient.unknownError', { + defaultMessage: `Report job {job} failed: Unknown error.`, + values: { job: jobId }, }); } diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 58fde5cbd83adc..661370446011f1 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -27,11 +27,10 @@ const mockJobsFound: Job[] = [ ].map((j) => new Job(j as ReportApiJSON)); // prettier-ignore const jobQueueClientMock = new ReportingAPIClient(coreMock.createSetup().http); -jobQueueClientMock.findForJobIds = async (jobIds: string[]) => mockJobsFound; +jobQueueClientMock.findForJobIds = async () => mockJobsFound; jobQueueClientMock.getInfo = () => Promise.resolve(({ content: 'this is the completed report data' } as unknown) as Job); -jobQueueClientMock.getError = () => - Promise.resolve({ content: 'this is the completed report data' }); +jobQueueClientMock.getError = () => Promise.resolve('this is the failed report error'); jobQueueClientMock.getManagementLink = () => '/#management'; jobQueueClientMock.getDownloadLink = () => '/reporting/download/job-123'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 8e41d34d054ecb..304b4fb73374df 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -74,9 +74,9 @@ export class ReportingNotifierStreamHandler { // no download link available for (const job of failedJobs) { - const { content } = await this.apiClient.getError(job.id); + const errorText = await this.apiClient.getError(job.id); this.notifications.toasts.addDanger( - getFailureToast(content, job, this.apiClient.getManagementLink) + getFailureToast(errorText, job, this.apiClient.getManagementLink) ); } return { completed: completedJobs, failed: failedJobs }; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap index 3417aa59f9d725..4ab50750bbc528 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap @@ -176,7 +176,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Job Info + Report info @@ -226,7 +226,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Job Info + Report info diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index d0ed2d737b5848..d78a09bc6af524 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -340,12 +340,14 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -521,15 +524,47 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
+ 2020-04-14 @ 05:01 PM + , + } + } > - Pending - waiting for job to be processed + Pending at + + 2020-04-14 @ 05:01 PM + + +
+ + + Waiting for job to be processed. + + +
+
@@ -648,6 +683,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -779,25 +815,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 0, "browserTimezone": "America/Phoenix", @@ -831,6 +849,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -852,7 +888,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } /> - - - - - @@ -1652,12 +1351,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -1833,14 +1535,15 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1849,13 +1552,30 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } > - Processing (attempt 1 of 1) at + Processing at 2020-04-14 @ 05:01 PM + +
+ + + Attempt 1 of 1. + + +
+
@@ -1974,6 +1694,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -2105,25 +1826,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -2157,10 +1860,28 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } - redirect={[MockFunction]} - toasts={ + license$={ Object { - "add": [MockFunction], + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], "addDanger": [MockFunction], "addError": [MockFunction], "addInfo": [MockFunction], @@ -2178,7 +1899,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } /> - - - - - @@ -2978,12 +2362,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -3159,11 +2546,12 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3431,25 +2820,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -3483,6 +2854,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -3551,7 +2940,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - @@ -4351,12 +3403,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -4532,14 +3587,15 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4548,7 +3604,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } > - Completed with warnings at + Completed at @@ -4567,13 +3623,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - Errors occurred: see job info for details. - + See report info for warnings.
@@ -4700,6 +3750,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -4831,25 +3882,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -4885,6 +3918,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` ], } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -4907,7 +3958,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -4953,7 +4004,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - - - - - - - - - - -
-
- - -
- - - -
- - - - -
- - -
- -
-
- - -
- - - - -
- Report -
-
-
-
- My Canvas Workpad -
- -
- - - canvas workpad - - -
-
-
-
- -
- - -
- Created at -
-
-
-
- 2020-04-14 @ 01:19 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:19 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:19 PM - - -
-
- -
- - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- -
- - - - - - -
- - -
- -
-
- - -
- - - - -
- Report -
-
-
-
- My Canvas Workpad -
- -
- - - canvas workpad - - -
-
-
-
- -
- - -
- Created at -
-
-
-
- 2020-04-14 @ 01:19 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:19 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:19 PM - - -
-
- -
- - -
- - -
-
- - - - - - - - - - - - - - @@ -8455,16 +4423,16 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
-
- - - - -
- Created at -
-
-
-
- 2020-04-14 @ 01:17 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:18 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:18 PM - - -
-
- -
- - -
- - + My Canvas Workpad +
+
-
- + - - - - - - - - - + +
+ +
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + @@ -9876,15 +5512,17 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-14 @ 01:12 PM + 2020-04-14 @ 01:19 PM
elastic @@ -10030,7 +5669,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-14 @ 01:13 PM + 2020-04-14 @ 01:19 PM , } } @@ -10077,7 +5717,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - 2020-04-14 @ 01:13 PM + 2020-04-14 @ 01:19 PM
@@ -10087,7 +5727,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` @@ -10117,12 +5757,12 @@ exports[`ReportListing Report job listing with some items 1`] = ` "attempts": 1, "browserTimezone": "America/Phoenix", "browser_type": "chromium", - "completed_at": "2020-04-14T17:13:03.719Z", + "completed_at": "2020-04-14T17:19:36.822Z", "content_type": "application/pdf", - "created_at": "2020-04-14T17:12:51.985Z", + "created_at": "2020-04-14T17:19:23.578Z", "created_by": "elastic", "csv_contains_formulas": undefined, - "id": "k905zdw11d34cbae0c3y6tzh", + "id": "k9067s1m1d4wcbae0cdnvcms", "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", @@ -10139,14 +5779,14 @@ exports[`ReportListing Report job listing with some items 1`] = ` "max_size_reached": undefined, "objectType": "canvas workpad", "size": 80262, - "started_at": "2020-04-14T17:12:52.431Z", + "started_at": "2020-04-14T17:19:25.247Z", "status": "completed", "timeout": 300000, "title": "My Canvas Workpad", "warnings": undefined, } } - itemId="k905zdw11d34cbae0c3y6tzh" + itemId="k9067s1m1d4wcbae0cdnvcms" key=".0" >
@@ -10329,127 +5970,920 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } + job={ + Job { + "attempts": 1, + "browserTimezone": "America/Phoenix", + "browser_type": "chromium", + "completed_at": "2020-04-14T17:19:36.822Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:19:23.578Z", + "created_by": "elastic", + "csv_contains_formulas": undefined, + "id": "k9067s1m1d4wcbae0cdnvcms", + "index": ".reporting-2020.04.12", + "isDeprecated": false, + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", + "layout": Object { + "dimensions": Object { + "height": 720, + "width": 1080, + }, + "id": "preserve_layout", + }, + "max_attempts": 1, + "max_size_reached": undefined, + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:19:25.247Z", + "status": "completed", + "timeout": 300000, + "title": "My Canvas Workpad", + "warnings": undefined, + } + } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + urlService={ + Object { + "locators": Object { + "get": [Function], + }, + } + } + > + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ +
+ + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + - - - - - - - - - + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:17 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:18 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:18 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + @@ -11249,15 +7594,17 @@ exports[`ReportListing Report job listing with some items 1`] = `
- count + My Canvas Workpad
- visualization + canvas workpad
@@ -11362,7 +7709,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-09 @ 03:09 PM + 2020-04-14 @ 01:12 PM
elastic @@ -11403,7 +7751,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-09 @ 03:10 PM + 2020-04-14 @ 01:13 PM , } } @@ -11450,7 +7799,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - 2020-04-09 @ 03:10 PM + 2020-04-14 @ 01:13 PM
@@ -11460,7 +7809,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` @@ -11490,36 +7839,36 @@ exports[`ReportListing Report job listing with some items 1`] = ` "attempts": 1, "browserTimezone": "America/Phoenix", "browser_type": "chromium", - "completed_at": "2020-04-09T19:10:10.049Z", - "content_type": "image/png", - "created_at": "2020-04-09T19:09:52.139Z", + "completed_at": "2020-04-14T17:13:03.719Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:12:51.985Z", "created_by": "elastic", "csv_contains_formulas": undefined, - "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", + "id": "k905zdw11d34cbae0c3y6tzh", + "index": ".reporting-2020.04.12", "isDeprecated": false, - "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, - "objectType": "visualization", - "size": 123456789, - "started_at": "2020-04-09T19:09:54.570Z", + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:12:52.431Z", "status": "completed", "timeout": 300000, - "title": "count", + "title": "My Canvas Workpad", "warnings": undefined, } } - itemId="k8t4ylcb07mi9d006214ifyg" + itemId="k905zdw11d34cbae0c3y6tzh" key=".0" >
@@ -11702,127 +8052,920 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } + job={ + Job { + "attempts": 1, + "browserTimezone": "America/Phoenix", + "browser_type": "chromium", + "completed_at": "2020-04-14T17:13:03.719Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:12:51.985Z", + "created_by": "elastic", + "csv_contains_formulas": undefined, + "id": "k905zdw11d34cbae0c3y6tzh", + "index": ".reporting-2020.04.12", + "isDeprecated": false, + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", + "layout": Object { + "dimensions": Object { + "height": 720, + "width": 1080, + }, + "id": "preserve_layout", + }, + "max_attempts": 1, + "max_size_reached": undefined, + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:12:52.431Z", + "status": "completed", + "timeout": 300000, + "title": "My Canvas Workpad", + "warnings": undefined, + } + } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + urlService={ + Object { + "locators": Object { + "get": [Function], + }, + } + } + > + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ count +
+ +
+ + - - - - - - - - - + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-09 @ 03:09 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-09 @ 03:10 PM + , + } + } + > + Completed at + + 2020-04-09 @ 03:10 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + diff --git a/x-pack/plugins/reporting/public/management/index.ts b/x-pack/plugins/reporting/public/management/index.ts index c107993ef3074f..4d324135288dbc 100644 --- a/x-pack/plugins/reporting/public/management/index.ts +++ b/x-pack/plugins/reporting/public/management/index.ts @@ -5,7 +5,25 @@ * 2.0. */ -export { ReportErrorButton } from './report_error_button'; -export { ReportDeleteButton } from './report_delete_button'; -export { ReportDownloadButton } from './report_download_button'; -export { ReportInfoButton } from './report_info_button'; +import { InjectedIntl } from '@kbn/i18n/react'; +import { ApplicationStart, ToastsSetup } from 'src/core/public'; +import { LicensingPluginSetup } from '../../../licensing/public'; +import { UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ClientConfigType } from '../plugin'; +import type { SharePluginSetup } from '../shared_imports'; + +export interface ListingProps { + intl: InjectedIntl; + apiClient: ReportingAPIClient; + capabilities: ApplicationStart['capabilities']; + license$: LicensingPluginSetup['license$']; // FIXME: license$ is deprecated + pollConfig: ClientConfigType['poll']; + redirect: ApplicationStart['navigateToApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; + toasts: ToastsSetup; + urlService: SharePluginSetup['url']; + ilmPolicyContextValue: UseIlmPolicyStatusReturn; +} + +export { ReportListing } from './report_listing'; diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index 20ea2988f3b8b8..0f0c06f8302059 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -16,7 +16,7 @@ import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context import { ClientConfigType } from '../plugin'; import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; import { KibanaContextProvider } from '../shared_imports'; -import { ReportListing } from './report_listing'; +import { ReportListing } from '.'; export async function mountManagementSection( coreSetup: CoreSetup, diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/report_delete_button.tsx index 7009a653c1bf60..da1ce9dd9e1cb6 100644 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_delete_button.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiConfirmModal } from '@elastic/eui'; import React, { Fragment, PureComponent } from 'react'; import { Job } from '../lib/job'; -import { Props as ListingProps } from './report_listing'; +import { ListingProps } from './'; type DeleteFn = () => Promise; type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; diff --git a/x-pack/plugins/reporting/public/management/report_download_button.tsx b/x-pack/plugins/reporting/public/management/report_download_button.tsx index b4212710377224..f21c83fbf42da5 100644 --- a/x-pack/plugins/reporting/public/management/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_download_button.tsx @@ -6,23 +6,28 @@ */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { InjectedIntl } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { JOB_STATUSES } from '../../common/constants'; import { Job as ListingJob } from '../lib/job'; -import { Props as ListingProps } from './report_listing'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; -type Props = { record: ListingJob } & ListingProps; +interface Props { + intl: InjectedIntl; + apiClient: ReportingAPIClient; + job: ListingJob; +} export const ReportDownloadButton: FunctionComponent = (props: Props) => { - const { record, apiClient, intl } = props; + const { job, apiClient, intl } = props; - if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { + if (job.status !== JOB_STATUSES.COMPLETED && job.status !== JOB_STATUSES.WARNINGS) { return null; } const button = ( apiClient.downloadReport(record.id)} + onClick={() => apiClient.downloadReport(job.id)} iconType="importAction" aria-label={intl.formatMessage({ id: 'xpack.reporting.listing.table.downloadReportAriaLabel', @@ -31,28 +36,14 @@ export const ReportDownloadButton: FunctionComponent = (props: Props) => /> ); - if (record.csv_contains_formulas) { + const warnings = job.getWarnings(); + if (warnings) { return ( - {button} - - ); - } - - if (record.max_size_reached) { - return ( - {button} diff --git a/x-pack/plugins/reporting/public/management/report_error_button.tsx b/x-pack/plugins/reporting/public/management/report_error_button.tsx deleted file mode 100644 index ee0c0e162cb7d0..00000000000000 --- a/x-pack/plugins/reporting/public/management/report_error_button.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { JOB_STATUSES } from '../../common/constants'; -import { Job as ListingJob } from '../lib/job'; -import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; - -interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - record: ListingJob; -} - -interface State { - isLoading: boolean; - isPopoverOpen: boolean; - calloutTitle: string; - error?: string; -} - -class ReportErrorButtonUi extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isPopoverOpen: false, - calloutTitle: props.intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToGenerateReportTitle', - defaultMessage: 'Unable to generate report', - }), - }; - } - - public render() { - const { record, intl } = this.props; - - if (record.status !== JOB_STATUSES.FAILED) { - return null; - } - - const button = ( - - ); - - return ( - - -

{this.state.error}

-
-
- ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - private togglePopover = () => { - this.setState((prevState) => { - return { isPopoverOpen: !prevState.isPopoverOpen }; - }); - - if (!this.state.error) { - this.loadError(); - } - }; - - private closePopover = () => { - this.setState({ isPopoverOpen: false }); - }; - - private loadError = async () => { - const { record, apiClient, intl } = this.props; - - this.setState({ isLoading: true }); - try { - const reportContent: JobContent = await apiClient.getError(record.id); - if (this.mounted) { - this.setState({ isLoading: false, error: reportContent.content }); - } - } catch (kfetchError) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle', - defaultMessage: 'Unable to fetch report content', - }), - error: kfetchError.message, - }); - } - } - }; -} - -export const ReportErrorButton = injectI18n(ReportErrorButtonUi); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx index 119856042a3266..147e18410200bc 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; +import { Job } from '../lib/job'; import { ReportInfoButton } from './report_info_button'; jest.mock('../lib/reporting_api_client'); @@ -15,16 +16,37 @@ import { ReportingAPIClient } from '../lib/reporting_api_client'; const httpSetup = {} as any; const apiClient = new ReportingAPIClient(httpSetup); +const job = new Job({ + id: 'abc-123', + index: '.reporting-2020.04.12', + migration_version: '7.15.0', + attempts: 0, + browser_type: 'chromium', + created_at: '2020-04-14T21:01:13.064Z', + created_by: 'elastic', + jobtype: 'printable_pdf', + max_attempts: 1, + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + payload: { + browserTimezone: 'America/Phoenix', + layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + process_expiration: '1970-01-01T00:00:00.000Z', + status: 'pending', + timeout: 300000, +}); describe('ReportInfoButton', () => { it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); expect(input).toMatchSnapshot(); }); it('opens flyout with info', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -33,7 +55,7 @@ describe('ReportInfoButton', () => { expect(flyout).toMatchSnapshot(); expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-456'); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); }); it('opens flyout with fetch error info', () => { @@ -42,7 +64,7 @@ describe('ReportInfoButton', () => { throw new Error('Could not fetch the job info'); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -51,6 +73,6 @@ describe('ReportInfoButton', () => { expect(flyout).toMatchSnapshot(); expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-789'); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); }); }); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx index 92acaa386bd568..8513558fb89ccc 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.tsx @@ -22,11 +22,11 @@ import React, { Component } from 'react'; import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; import { Job } from '../lib/job'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { Props as ListingProps } from './report_listing'; +import { ListingProps } from '.'; interface Props extends Pick { - jobId: string; apiClient: ReportingAPIClient; + job: Job; } interface State { @@ -58,7 +58,10 @@ class ReportInfoButtonUi extends Component { this.state = { isLoading: false, isFlyoutVisible: false, - calloutTitle: 'Job Info', + calloutTitle: props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportCalloutTitle', + defaultMessage: 'Report info', + }), info: null, error: null, }; @@ -76,58 +79,177 @@ class ReportInfoButtonUi extends Component { return null; } - const jobType = info.jobtype || NA; - const attempts = info.attempts ? info.attempts.toString() : NA; - const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; const timeout = info.timeout ? info.timeout.toString() : NA; - const warnings = info.warnings?.join(',') ?? null; const jobInfo = [ - { title: 'Title', description: info.title || NA }, - { title: 'Created By', description: info.created_by || NA }, - { title: 'Created At', description: info.created_at || NA }, - { title: 'Timezone', description: info.browserTimezone || NA }, - { title: 'Status', description: info.status || NA }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.titleInfo', + defaultMessage: 'Title', + }), + description: info.title || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.createdAtInfo', + defaultMessage: 'Created At', + }), + description: info.getCreatedAtLabel(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.statusInfo', + defaultMessage: 'Status', + }), + description: info.getStatus(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.tzInfo', + defaultMessage: 'Timezone', + }), + description: info.browserTimezone || NA, + }, ]; const processingInfo = [ - { title: 'Started At', description: info.started_at || NA }, - { title: 'Completed At', description: info.completed_at || NA }, { - title: 'Processed By', + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.startedAtInfo', + defaultMessage: 'Started At', + }), + description: info.started_at || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.completedAtInfo', + defaultMessage: 'Completed At', + }), + description: info.completed_at || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.processedByInfo', + defaultMessage: 'Processed By', + }), description: info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, }, - { title: 'Content Type', description: info.content_type || NA }, - { title: 'Size in Bytes', description: info.size?.toString() || NA }, - { title: 'Attempts', description: attempts }, - { title: 'Max Attempts', description: maxAttempts }, - { title: 'Timeout', description: timeout }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', + defaultMessage: 'Content Type', + }), + description: info.content_type || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.sizeInfo', + defaultMessage: 'Size in Bytes', + }), + description: info.size?.toString() || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.attemptsInfo', + defaultMessage: 'Attempts', + }), + description: info.attempts.toString(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', + defaultMessage: 'Max Attempts', + }), + description: info.max_attempts?.toString() || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.timeoutInfo', + defaultMessage: 'Timeout', + }), + description: timeout, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', + defaultMessage: 'Export Type', + }), + description: info.isDeprecated + ? this.props.intl.formatMessage( + { + id: 'xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', + defaultMessage: '{jobtype} (DEPRECATED)', + }, + { jobtype: info.jobtype } + ) + : info.jobtype, + }, + + // TODO when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field ]; const jobScreenshot = [ - { title: 'Dimensions', description: getDimensions(info) }, - { title: 'Layout', description: info.layout?.id || UNKNOWN }, - { title: 'Browser Type', description: info.browser_type || NA }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.dimensionsInfo', + defaultMessage: 'Dimensions', + }), + description: getDimensions(info), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.layoutInfo', + defaultMessage: 'Layout', + }), + description: info.layout?.id || UNKNOWN, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', + defaultMessage: 'Browser Type', + }), + description: info.browser_type || NA, + }, + ]; + + const warnings = info.getWarnings(); + const warningsInfo = warnings && [ + { + title: Warnings, + description: {warnings}, + }, ]; - const warningInfo = warnings && [{ title: 'Errors', description: warnings }]; + const errored = info.getError(); + const errorInfo = errored && [ + { + title: Error, + description: {errored}, + }, + ]; return ( <> - {USES_HEADLESS_JOB_TYPES.includes(jobType) ? ( + {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( <> ) : null} - {warningInfo ? ( + {warningsInfo ? ( + <> + + + + ) : null} + {errorInfo ? ( <> - + ) : null} @@ -143,6 +265,7 @@ class ReportInfoButtonUi extends Component { } public render() { + const job = this.props.job; let flyout; if (this.state.isFlyoutVisible) { @@ -168,21 +291,44 @@ class ReportInfoButtonUi extends Component { ); } + let message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', + defaultMessage: 'See report info', + }); + if (job.getError()) { + message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', + defaultMessage: 'See report info and error message', + }); + } else if (job.getWarnings()) { + message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', + defaultMessage: 'See report info and warnings', + }); + } + + let buttonIconType = 'iInCircle'; + let buttonColor: 'primary' | 'danger' | 'warning' = 'primary'; + if (job.getWarnings() || job.getError()) { + buttonIconType = 'alert'; + buttonColor = 'danger'; + } + if (job.getWarnings()) { + buttonColor = 'warning'; + } + return ( <> - + {flyout} @@ -193,7 +339,7 @@ class ReportInfoButtonUi extends Component { private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info = await this.props.apiClient.getInfo(this.props.jobId); + const info = await this.props.apiClient.getInfo(this.props.job.id); if (this.mounted) { this.setState({ isLoading: false, info }); } @@ -201,7 +347,10 @@ class ReportInfoButtonUi extends Component { if (this.mounted) { this.setState({ isLoading: false, - calloutTitle: 'Unable to fetch report info', + calloutTitle: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', + defaultMessage: 'Unable to fetch report info', + }), info: null, error: err, }); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index b2eb6f0029580a..47969edb72fdad 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -23,7 +23,7 @@ import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context import { Job } from '../lib/job'; import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; import { KibanaContextProvider } from '../shared_imports'; -import { Props, ReportListing } from './report_listing'; +import { ListingProps as Props, ReportListing } from '.'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 9ba0026999137a..4e183380a6b41e 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -16,39 +16,25 @@ import { EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; -import { ApplicationStart, ToastsSetup } from 'src/core/public'; -import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; -import { JOB_STATUSES as JobStatuses } from '../../common/constants'; +import { ILicense } from '../../../licensing/public'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { useIlmPolicyStatus, UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; import { Job } from '../lib/job'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient, useInternalApiClient } from '../lib/reporting_api_client'; -import { ClientConfigType } from '../plugin'; -import type { SharePluginSetup } from '../shared_imports'; +import { useInternalApiClient } from '../lib/reporting_api_client'; import { useKibana } from '../shared_imports'; -import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; import { IlmPolicyLink } from './ilm_policy_link'; import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import { ReportDeleteButton } from './report_delete_button'; import { ReportDiagnostic } from './report_diagnostic'; - -export interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - capabilities: ApplicationStart['capabilities']; - license$: LicensingPluginSetup['license$']; - pollConfig: ClientConfigType['poll']; - redirect: ApplicationStart['navigateToApp']; - navigateToUrl: ApplicationStart['navigateToUrl']; - toasts: ToastsSetup; - urlService: SharePluginSetup['url']; - ilmPolicyContextValue: UseIlmPolicyStatusReturn; -} +import { ReportDownloadButton } from './report_download_button'; +import { ReportInfoButton } from './report_info_button'; +import { ListingProps as Props } from './'; interface State { page: number; @@ -61,45 +47,6 @@ interface State { badLicenseMessage: string; } -const jobStatusLabelsMap = new Map([ - [ - JobStatuses.PENDING, - i18n.translate('xpack.reporting.jobStatuses.pendingText', { - defaultMessage: 'Pending', - }), - ], - [ - JobStatuses.PROCESSING, - i18n.translate('xpack.reporting.jobStatuses.processingText', { - defaultMessage: 'Processing', - }), - ], - [ - JobStatuses.COMPLETED, - i18n.translate('xpack.reporting.jobStatuses.completedText', { - defaultMessage: 'Completed', - }), - ], - [ - JobStatuses.WARNINGS, - i18n.translate('xpack.reporting.jobStatuses.warningText', { - defaultMessage: 'Completed with warnings', - }), - ], - [ - JobStatuses.FAILED, - i18n.translate('xpack.reporting.jobStatuses.failedText', { - defaultMessage: 'Failed', - }), - ], - [ - JobStatuses.CANCELLED, - i18n.translate('xpack.reporting.jobStatuses.cancelledText', { - defaultMessage: 'Cancelled', - }), - ], -]); - class ReportListingUi extends Component { private isInitialJobsFetch: boolean; private licenseSubscription?: Subscription; @@ -212,9 +159,9 @@ class ReportListingUi extends Component { this.setState((current) => ({ ...current, selectedJobs: jobs })); }; - private removeRecord = (record: Job) => { + private removeJob = (job: Job) => { const { jobs } = this.state; - const filtered = jobs.filter((j) => j.id !== record.id); + const filtered = jobs.filter((j) => j.id !== job.id); this.setState((current) => ({ ...current, jobs: filtered })); }; @@ -223,17 +170,17 @@ class ReportListingUi extends Component { if (selectedJobs.length === 0) return undefined; const performDelete = async () => { - for (const record of selectedJobs) { + for (const job of selectedJobs) { try { - await this.props.apiClient.deleteReport(record.id); - this.removeRecord(record); + await this.props.apiClient.deleteReport(job.id); + this.removeJob(job); this.props.toasts.addSuccess( this.props.intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteConfim', defaultMessage: `The {reportTitle} report was deleted`, }, - { reportTitle: record.title } + { reportTitle: job.title } ) ); } catch (error) { @@ -316,15 +263,6 @@ class ReportListingUi extends Component { return this.state.showLinks && this.state.enableLinks; }; - private formatDate(timestamp: string) { - try { - return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); - } catch (error) { - // ignore parse error and display unformatted value - return timestamp; - } - } - private renderTable() { const { intl } = this.props; @@ -335,12 +273,12 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.reportTitle', defaultMessage: 'Report', }), - render: (objectTitle: string, record: Job) => { + render: (objectTitle: string, job: Job) => { return (
{objectTitle}
- {record.objectType} + {job.objectType}
); @@ -352,17 +290,9 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.createdAtTitle', defaultMessage: 'Created at', }), - render: (createdAt: string, record: Job) => { - if (record.created_by) { - return ( -
-
{this.formatDate(createdAt)}
- {record.created_by} -
- ); - } - return this.formatDate(createdAt); - }, + render: (_createdAt: string, job: Job) => ( +
{job.getCreatedAtLabel()}
+ ), }, { field: 'status', @@ -370,89 +300,9 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.statusTitle', defaultMessage: 'Status', }), - render: (status: string, record: Job) => { - if (status === 'pending') { - return ( -
- -
- ); - } - - let maxSizeReached; - if (record.max_size_reached) { - maxSizeReached = ( - - - - ); - } - - let warnings; - if (record.warnings) { - warnings = ( - - - - - - ); - } - - let statusTimestamp; - if (status === JobStatuses.PROCESSING && record.started_at) { - statusTimestamp = this.formatDate(record.started_at); - } else if ( - record.completed_at && - ([ - JobStatuses.COMPLETED, - JobStatuses.FAILED, - JobStatuses.WARNINGS, - ] as string[]).includes(status) - ) { - statusTimestamp = this.formatDate(record.completed_at); - } - - let statusLabel = jobStatusLabelsMap.get(status as JobStatuses) || status; - - if (status === JobStatuses.PROCESSING) { - statusLabel = statusLabel + ` (attempt ${record.attempts} of ${record.max_attempts})`; - } - - if (statusTimestamp) { - return ( -
- {statusTimestamp}, - }} - /> - {maxSizeReached} - {warnings} -
- ); - } - - // unknown status - return ( -
- {statusLabel} - {maxSizeReached} -
- ); - }, + render: (_status: string, job: Job) => ( +
{job.getStatusLabel()}
+ ), }, { name: intl.formatMessage({ @@ -461,12 +311,11 @@ class ReportListingUi extends Component { }), actions: [ { - render: (record: Job) => { + render: (job: Job) => { return ( -
- - - +
+ +
); }, @@ -520,7 +369,8 @@ class ReportListingUi extends Component { selection={selection} isSelectable={true} onChange={this.onTableChange} - data-test-subj="reportJobListing" + data-test-subj={REPORT_TABLE_ID} + rowProps={() => ({ 'data-test-subj': REPORT_TABLE_ROW_ID })} /> ); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index eb2abf4036c03d..7aaa9c78602a90 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -17,7 +17,6 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; import { ReportingCore } from '../../..'; -import { BROWSER_TYPE } from '../../../../common/constants'; import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; @@ -70,7 +69,7 @@ export class HeadlessChromiumDriverFactory { }); } - type = BROWSER_TYPE; + type = 'chromium'; /* * Return an observable to objects which will drive screenshot capture for a page diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 0d0332983d6bc3..37557c3afb0c77 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -93,49 +93,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }) ); - // return the raw output from a job - router.get( - { - path: `${MAIN_ENTRY}/output/{docId}`, - validate: { - params: schema.object({ - docId: schema.string({ minLength: 2 }), - }), - }, - }, - userHandler(async (user, context, req, res) => { - // ensure the async dependencies are loaded - if (!context.reporting) { - return handleUnavailable(res); - } - - const { docId } = req.params; - const { - management: { jobTypes = [] }, - } = await reporting.getLicenseInfo(); - - const jobsQuery = jobsQueryFactory(reporting); - const result = await jobsQuery.getContent(user, docId); - - if (!result) { - throw Boom.notFound(); - } - - const { jobtype: jobType, output } = result; - - if (!jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } - - return res.ok({ - body: output?.content ?? {}, - headers: { - 'content-type': 'application/json', - }, - }); - }) - ); - // return some info about the job router.get( { diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 76896a7472d59d..a0d6962074a708 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -12,7 +12,8 @@ import { i18n } from '@kbn/i18n'; import { UnwrapPromise } from '@kbn/utility-types'; import { ElasticsearchClient } from 'src/core/server'; import { ReportingCore } from '../../'; -import { ReportApiJSON, ReportDocument, ReportSource } from '../../../common/types'; +import { JobContent, ReportApiJSON, ReportDocument, ReportSource } from '../../../common/types'; +import { statuses } from '../../lib/statuses'; import { Report } from '../../lib/store'; import { ReportingUser } from '../../types'; @@ -47,6 +48,7 @@ interface JobsQueryFactory { count(jobTypes: string[], user: ReportingUser): Promise; get(user: ReportingUser, id: string): Promise; getContent(user: ReportingUser, id: string): Promise; + getError(user: ReportingUser, id: string): Promise<(ReportContent & JobContent) | void>; delete(deleteIndex: string, id: string): Promise>; } @@ -205,6 +207,20 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory }; }, + async getError(user, id) { + const content = await this.getContent(user, id); + if (content && content?.output?.content) { + if (content.status !== statuses.JOB_STATUS_FAILED) { + throw new Error(`Can not get error for ${id}`); + } + return { + ...content, + content: content.output.content, + content_type: false, + }; + } + }, + async delete(deleteIndex, id) { try { const { asInternalUser: elasticsearchClient } = await reportingCore.getEsClient(); diff --git a/x-pack/plugins/rule_registry/server/mocks.ts b/x-pack/plugins/rule_registry/server/mocks.ts new file mode 100644 index 00000000000000..cc5c3cfd484a7c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services_mock'; + +export const ruleRegistryMocks = { + createLifecycleAlertServices: createLifecycleAlertServicesMock, +}; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts new file mode 100644 index 00000000000000..c519674569a51b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../../../../plugins/alerting/server'; +import { AlertExecutorOptionsWithExtraServices } from '../types'; + +import { LifecycleAlertServices, LifecycleRuleExecutor } from './create_lifecycle_executor'; + +export const createLifecycleRuleExecutorMock = < + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + executor: LifecycleRuleExecutor +) => async ( + options: AlertExecutorOptionsWithExtraServices< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + LifecycleAlertServices + > +) => await executor(options); diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts new file mode 100644 index 00000000000000..37b4847bc9c695 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertInstanceContext, AlertInstanceState } from '../../../alerting/server'; +import { alertsMock } from '../../../alerting/server/mocks'; +import { LifecycleAlertServices } from './create_lifecycle_executor'; + +/** + * This wraps the alerts to enable the preservation of the generic type + * arguments of the factory function. + **/ +class AlertsMockWrapper< + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +> { + createAlertServices() { + return alertsMock.createAlertServices(); + } +} + +type AlertServices< + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +> = ReturnType['createAlertServices']>; + +export const createLifecycleAlertServicesMock = < + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + alertServices: AlertServices +): LifecycleAlertServices => ({ + alertWithLifecycle: ({ id }) => alertServices.alertInstanceFactory(id), +}); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index f2d3fcd6ab3caf..adaca23be5dae5 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -64,6 +64,7 @@ describe('SecurityNavControl', () => { onClick={[Function]} > diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 56eb784467c056..3b45b5164c6cfb 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -100,7 +100,7 @@ export class SecurityNavControl extends Component { (authenticatedUser && (authenticatedUser.full_name || authenticatedUser.username)) || ''; const buttonContents = authenticatedUser ? ( - + ) : ( ); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts index ece00bcd435783..9839d29291629a 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts @@ -86,6 +86,7 @@ describe('SecurityNavControlService', () => {
diff --git a/x-pack/plugins/security/server/errors.test.ts b/x-pack/plugins/security/server/errors.test.ts index 9aa86357932810..90860689b2852a 100644 --- a/x-pack/plugins/security/server/errors.test.ts +++ b/x-pack/plugins/security/server/errors.test.ts @@ -7,7 +7,6 @@ import { errors as esErrors } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; -import { errors as legacyESErrors } from 'elasticsearch'; import * as errors from './errors'; import { securityMock } from './mocks'; @@ -72,11 +71,6 @@ describe('lib/errors', () => { ).toBe(401); }); - it('extracts status code from legacy Elasticsearch client error', () => { - expect(errors.getErrorStatusCode(new legacyESErrors.BadRequest())).toBe(400); - expect(errors.getErrorStatusCode(new legacyESErrors.AuthenticationException())).toBe(401); - }); - it('extracts status code from `status` property', () => { expect(errors.getErrorStatusCode({ statusText: 'Bad Request', status: 400 })).toBe(400); expect(errors.getErrorStatusCode({ statusText: 'Unauthorized', status: 401 })).toBe(401); @@ -120,13 +114,6 @@ describe('lib/errors', () => { ).toBe(JSON.stringify({ field1: 'value-1', field2: 'value-2' })); }); - it('extracts status code from legacy Elasticsearch client error', () => { - expect(errors.getDetailedErrorMessage(new legacyESErrors.BadRequest())).toBe('Bad Request'); - expect(errors.getDetailedErrorMessage(new legacyESErrors.AuthenticationException())).toBe( - 'Authentication Exception' - ); - }); - it('extracts `message` property', () => { expect(errors.getDetailedErrorMessage(new Error('some-message'))).toBe('some-message'); }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index fba7bf4f872e7f..35972e103855e9 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; import type { ObjectType } from '@kbn/config-schema'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -18,6 +18,7 @@ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user. import { AuthenticationResult } from '../../authentication'; import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; +import { securityMock } from '../../mocks'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; @@ -109,9 +110,9 @@ describe('Change password', () => { }); it('returns 403 if old password is wrong.', async () => { - const changePasswordFailure = new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }); + const changePasswordFailure = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( changePasswordFailure ); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts index 0959f999a4b53c..ced815c4b58c16 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts @@ -31,27 +31,31 @@ describe('Cases connector incident fields', () => { beforeEach(() => { cleanKibana(); cy.intercept('GET', '/api/cases/configure/connectors/_find', getMockConnectorsResponse()); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().sn}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/connector/${getConnectorIds().sn}/_execute`, (req) => { const response = req.body.params.subAction === 'getChoices' ? getExecuteResponses().servicenow.choices : { status: 'ok', data: [] }; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().jira}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/connector/${getConnectorIds().jira}/_execute`, (req) => { const response = req.body.params.subAction === 'issueTypes' ? getExecuteResponses().jira.issueTypes : getExecuteResponses().jira.fieldsByIssueType; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().resilient}/_execute`, (req) => { - const response = - req.body.params.subAction === 'incidentTypes' - ? getExecuteResponses().resilient.incidentTypes - : getExecuteResponses().resilient.severity; - req.reply(response); - }); + cy.intercept( + 'POST', + `/api/actions/connector/${getConnectorIds().resilient}/_execute`, + (req) => { + const response = + req.body.params.subAction === 'incidentTypes' + ? getExecuteResponses().resilient.incidentTypes + : getExecuteResponses().resilient.severity; + req.reply(response); + } + ); }); it('Correct incident fields show when connector is changed', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts index a89ddf3e0b250b..5e851cecbd86ba 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts @@ -44,7 +44,7 @@ describe('timeline data providers', () => { closeTimeline(); }); - it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { + it.skip('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { dragAndDropFirstHostToTimeline(); openTimelineUsingToggle(); cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) @@ -78,7 +78,7 @@ describe('timeline data providers', () => { }); }); - it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { + it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { dragFirstHostToTimeline(); cy.get(IS_DRAGGING_DATA_PROVIDERS) @@ -87,7 +87,7 @@ describe('timeline data providers', () => { .should('have.class', 'drop-target-data-providers'); }); - it('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => { + it.skip('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => { dragFirstHostToEmptyTimelineDataProviders(); cy.get(IS_DRAGGING_DATA_PROVIDERS) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts index 38c6f41f1049c2..ac34d65f0fd0a1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts @@ -79,7 +79,7 @@ describe('timeline flyout button', () => { closeTimelineUsingCloseButton(); }); - it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { + it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { dragFirstHostToTimeline(); cy.get(IS_DRAGGING_DATA_PROVIDERS) diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts index 615421b1ef3234..cf1bac421b447a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts @@ -7,6 +7,6 @@ export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; -export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"] a.euiLink'; +export const HOSTS_NAMES = '[data-test-subj="render-content-host.name"] a.euiLink'; -export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="draggable-content-host.name"]'; +export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="render-content-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts index 536379654862c6..f2a712f8688505 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts @@ -5,6 +5,6 @@ * 2.0. */ -export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]'; +export const PROCESS_NAME_FIELD = '[data-test-subj="render-content-process.name"]'; export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 6d87b5d3a68b90..ad15f0a5fa9fb2 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -340,13 +340,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { const dataProviderId = `draggableId.content.draggable-legend-item-uuid_v4()-${escapeDataProviderId( stackByField )}-${escapeDataProviderId(datum.key)}`; - - expect( - wrapper - .find(`[draggableId="${dataProviderId}"] [data-test-subj="providerContainer"]`) - .first() - .text() - ).toEqual(datum.key); + expect(wrapper.find(`div[data-provider-id="${dataProviderId}"]`).first().text()).toEqual( + datum.key + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx index 6017501f87dcc8..493ce4da78eba7 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx @@ -46,6 +46,7 @@ const DraggableLegendItemComponent: React.FC<{ data-test-subj={`legend-item-${dataProviderId}`} field={field} id={dataProviderId} + isDraggable={false} timelineId={timelineId} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap index aa8214938c2b08..0b25ff2c8c5eed 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap @@ -17,6 +17,7 @@ exports[`DraggableWrapper rendering it renders against the snapshot 1`] = ` }, } } + isDraggable={true} render={[Function]} /> `; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index bdc5545880e1c2..d27ad96ff3c4f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -42,7 +42,11 @@ describe('DraggableWrapper', () => { const wrapper = shallow( - message} /> + message} + /> ); @@ -54,7 +58,11 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); @@ -66,19 +74,27 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(false); }); test('it renders hover actions when the mouse is over the text of draggable wrapper', async () => { const wrapper = mount( - message} /> + message} + /> ); @@ -88,7 +104,7 @@ describe('DraggableWrapper', () => { wrapper.update(); jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); }); }); }); @@ -98,7 +114,12 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} truncate /> + message} + truncate + /> ); @@ -112,7 +133,11 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); 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 9db5b3899d8bc1..d008aad2133923 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 @@ -7,7 +7,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Draggable, DraggableProvided, @@ -25,12 +25,13 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com 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'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; +import { useHoverActions } from '../hover_actions/use_hover_actions'; // As right now, we do not know what we want there, we will keep it as a placeholder export const DragEffects = styled.div``; @@ -80,7 +81,7 @@ const Wrapper = styled.div` Wrapper.displayName = 'Wrapper'; -const ProviderContentWrapper = styled.span` +export const ProviderContentWrapper = styled.span` > span.euiToolTipAnchor { display: block; /* allow EuiTooltip content to be truncatable */ } @@ -95,6 +96,7 @@ type RenderFunctionProp = ( interface Props { dataProvider: DataProvider; disabled?: boolean; + isDraggable?: boolean; inline?: boolean; render: RenderFunctionProp; timelineId?: string; @@ -121,55 +123,35 @@ export const getStyle = ( }; }; -const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => { - const links = draggableElement?.querySelectorAll('.euiLink') ?? []; - return links.length > 0; -}; - -const DraggableWrapperComponent: React.FC = ({ +const DraggableOnWrapperComponent: React.FC = ({ dataProvider, onFilterAdded, render, timelineId, truncate, }) => { - const keyboardHandlerRef = useRef(null); - 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 [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); const dispatch = useDispatch(); const { timelines } = useKibana().services; - - const handleClosePopOverTrigger = useCallback(() => { - setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); - setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { - if (prevHoverActionsOwnFocus) { - setTimeout(() => { - keyboardHandlerRef.current?.focus(); - }, 0); - } - return false; // always give up ownership - }); - - setTimeout(() => { - setHoverActionsOwnFocus(false); - }, 0); // invoked on the next tick, because we want to restore focus first - }, [keyboardHandlerRef]); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); + const { + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + keyboardHandlerRef, + onCloseRequested, + openPopover, + onFocus, + setContainerRef, + showTopN, + } = useHoverActions({ + dataProvider, + onFilterAdded, + render, + timelineId, + truncate, + }); const registerProvider = useCallback(() => { if (!isDisabled) { @@ -192,49 +174,6 @@ const DraggableWrapperComponent: React.FC = ({ [unRegisterProvider] ); - const hoverContent = useMemo(() => { - // display links as additional content in the hover menu to enable keyboard - // navigation of links (when the draggable contains them): - const additionalContent = - hoverActionsOwnFocus && !showTopN && draggableContainsLinks(draggableRef.current) ? ( - - {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} - - ) : null; - - return ( - - ); - }, [ - dataProvider, - handleClosePopOverTrigger, - hoverActionsOwnFocus, - onFilterAdded, - render, - showTopN, - timelineId, - timelineIdFind, - toggleTopN, - ]); - const RenderClone = useCallback( (provided, snapshot) => ( @@ -264,7 +203,7 @@ const DraggableWrapperComponent: React.FC = ({ {...provided.dragHandleProps} ref={(e: HTMLDivElement) => { provided.innerRef(e); - draggableRef.current = e; + setContainerRef(e); }} data-test-subj="providerContainer" isDragging={snapshot.isDragging} @@ -292,13 +231,9 @@ const DraggableWrapperComponent: React.FC = ({ )} ), - [dataProvider, registerProvider, render, truncate] + [dataProvider, registerProvider, render, setContainerRef, truncate] ); - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, []); - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId: getDraggableId(dataProvider.id), @@ -307,24 +242,6 @@ const DraggableWrapperComponent: React.FC = ({ openPopover, }); - const onFocus = useCallback(() => { - if (!hoverActionsOwnFocus) { - keyboardHandlerRef.current?.focus(); - } - }, [hoverActionsOwnFocus, keyboardHandlerRef]); - - const onCloseRequested = useCallback(() => { - setShowTopN(false); - - if (hoverActionsOwnFocus) { - setHoverActionsOwnFocus(false); - - setTimeout(() => { - onFocus(); // return focus to this draggable on the next tick, because we owned focus - }, 0); - } - }, [onFocus, hoverActionsOwnFocus]); - const DroppableContent = useCallback( (droppableProvided) => (
@@ -350,7 +267,7 @@ const DraggableWrapperComponent: React.FC = ({ {droppableProvided.placeholder}
), - [DraggableContent, dataProvider.id, isDisabled, onBlur, onFocus, onKeyDown] + [DraggableContent, dataProvider.id, isDisabled, keyboardHandlerRef, onBlur, onFocus, onKeyDown] ); const content = useMemo( @@ -385,6 +302,75 @@ const DraggableWrapperComponent: React.FC = ({ ); }; +const DraggableWrapperComponent: React.FC = ({ + dataProvider, + isDraggable = false, + onFilterAdded, + render, + timelineId, + truncate, +}) => { + const { + closePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + onCloseRequested, + setContainerRef, + showTopN, + } = useHoverActions({ + dataProvider, + isDraggable, + onFilterAdded, + render, + timelineId, + truncate, + }); + const renderContent = useCallback( + () => ( +
{ + setContainerRef(e); + }} + tabIndex={-1} + data-provider-id={getDraggableId(dataProvider.id)} + > + {truncate ? ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + ) : ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + )} +
+ ), + [dataProvider, render, setContainerRef, truncate] + ); + if (!isDraggable) { + return ( + + ); + } + return ( + + ); +}; + export const DraggableWrapper = React.memo(DraggableWrapperComponent); DraggableWrapper.displayName = 'DraggableWrapper'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx deleted file mode 100644 index 2531780ec4bd5d..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mount, ReactWrapper } from 'enzyme'; - -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { mockBrowserFields } from '../../containers/source/mock'; -import '../../mock/match_media'; -import { useKibana } from '../../lib/kibana'; -import { TestProviders } from '../../mock'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { useSourcererScope } from '../../containers/sourcerer'; -import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; -import { TimelineId } from '../../../../common/types/timeline'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; - -jest.mock('../link_to'); -jest.mock('../../lib/kibana'); -jest.mock('../../containers/sourcerer', () => { - const original = jest.requireActual('../../containers/sourcerer'); - - return { - ...original, - useSourcererScope: jest.fn(), - }; -}); - -jest.mock('uuid', () => { - return { - v1: jest.fn(() => 'uuid.v1()'), - v4: jest.fn(() => 'uuid.v4()'), - }; -}); -const mockStartDragToTimeline = jest.fn(); -jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => { - const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline'); - return { - ...original, - useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }), - }; -}); -const mockAddFilters = jest.fn(); -jest.mock('../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), - useDeepEqualSelector: jest.fn(), -})); -jest.mock('../../../common/hooks/use_invalid_filter_query.tsx'); - -const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; -const timelineId = TimelineId.active; -const field = 'process.name'; -const value = 'nice'; -const toggleTopN = jest.fn(); -const goGetTimelineId = jest.fn(); -const defaultProps = { - field, - goGetTimelineId, - ownFocus: false, - showTopN: false, - timelineId, - toggleTopN, - value, -}; - -describe('DraggableWrapperHoverContent', () => { - beforeAll(() => { - mockStartDragToTimeline.mockReset(); - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - filterManager: { addFilters: mockAddFilters }, - }); - (useSourcererScope as jest.Mock).mockReturnValue({ - browserFields: mockBrowserFields, - selectedPatterns: [], - indexPattern: {}, - }); - }); - - /** - * The tests for "Filter for value" and "Filter out value" are similar enough - * to combine them into "table tests" using this array - */ - const forOrOut = ['for', 'out']; - - forOrOut.forEach((hoverAction) => { - describe(`Filter ${hoverAction} value`, () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() - ).toBe(true); - }); - - test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() - ).toBe(false); - }); - - test(`it should call goGetTimelineId when user is over the 'Filter ${hoverAction} value' button`, () => { - const wrapper = mount( - - - - ); - const button = wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first(); - button.simulate('mouseenter'); - expect(goGetTimelineId).toHaveBeenCalledWith(true); - }); - - describe('when run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - - beforeEach(() => { - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(mockAddFilters).toBeCalledWith({ - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: hoverAction === 'out' ? true : false, - params: { query: 'nice' }, - type: 'phrase', - value: 'nice', - }, - query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, - }); - }); - - test('when clicked, invokes onFilterAdded when running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(onFilterAdded).toBeCalled(); - }); - }); - - describe('when NOT run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - const kibana = useKibana(); - - beforeEach(() => { - kibana.services.data.query.filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - test('when clicked, it adds a filter to the global filters when NOT running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith({ - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: hoverAction === 'out' ? true : false, - params: { query: 'nice' }, - type: 'phrase', - value: 'nice', - }, - query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, - }); - }); - - test('when clicked, invokes onFilterAdded when NOT running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(onFilterAdded).toBeCalled(); - }); - }); - - describe('an empty string value when run in the context of a timeline', () => { - let filterManager: FilterManager; - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - - beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - const expectedFilterTypeDescription = - hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; - test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the timeline when run in the context of a timeline`, () => { - const expected = - hoverAction === 'for' - ? { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: true, - type: 'exists', - value: 'exists', - }, - } - : { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: false, - type: 'exists', - value: 'exists', - }, - }; - - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(mockAddFilters).toBeCalledWith(expected); - }); - }); - - describe('an empty string value when NOT run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - const kibana = useKibana(); - - beforeEach(() => { - kibana.services.data.query.filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - const expectedFilterTypeDescription = - hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; - test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the global filters when NOT running in the context of a timeline`, () => { - const expected = - hoverAction === 'for' - ? { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: true, - type: 'exists', - value: 'exists', - }, - } - : { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: false, - type: 'exists', - value: 'exists', - }, - }; - - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith(expected); - }); - }); - }); - }); - - describe('Add to timeline', () => { - const aggregatableStringField = 'cloud.account.id'; - const draggableId = 'draggable.id'; - - [false, true].forEach((showTopN) => { - [value, null].forEach((maybeValue) => { - [draggableId, undefined].forEach((maybeDraggableId) => { - const shouldRender = !showTopN && maybeValue != null && maybeDraggableId != null; - const assertion = shouldRender ? 'should render' : 'should NOT render'; - - test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-to-timeline"]').first().exists()).toBe( - shouldRender - ); - }); - }); - }); - }); - - test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', async () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="add-to-timeline"]').first().simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(mockStartDragToTimeline).toHaveBeenCalled(); - }); - }); - }); - - describe('Top N', () => { - test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, () => { - const aggregatableStringField = 'cloud.account.id'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); - }); - - test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); - }); - - test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, () => { - const notKnownToBrowserFields = 'unknown.field'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); - }); - - test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, async () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - const button = wrapper.find(`[data-test-subj="show-top-field"]`).first(); - button.simulate('mouseenter'); - await waitFor(() => { - expect(goGetTimelineId).toHaveBeenCalledWith(true); - }); - }); - - test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - wrapper.find('[data-test-subj="show-top-field"]').first().simulate('click'); - wrapper.update(); - - expect(toggleTopN).toBeCalled(); - }); - - test(`it does NOT render the Top N histogram when when showTopN is false`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="eventsByDatasetOverviewPanel"]').first().exists()).toBe( - false - ); - }); - - test(`it does NOT render the 'Show top field' button when showTopN is true`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); - }); - - test(`it renders the Top N histogram when when showTopN is true`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').first().exists() - ).toBe(true); - }); - }); - - describe('Copy to Clipboard', () => { - test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(true); - }); - - test(`it does NOT render the 'Copy to Clipboard' button when showTopN is true`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx deleted file mode 100644 index 71c3114015a03f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiFocusTrap, - EuiPanel, - EuiScreenReaderOnly, - EuiToolTip, -} from '@elastic/eui'; - -import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'; -import { DraggableId } from 'react-beautiful-dnd'; -import styled from 'styled-components'; - -import { getAllFieldsByName } from '../../containers/source'; -import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { useKibana } from '../../lib/kibana'; -import { createFilter } from '../add_filter_to_global_search_bar'; -import { StatefulTopN } from '../top_n'; - -import { allowTopN } from './helpers'; -import * as i18n from './translations'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { TimelineId } from '../../../../common/types/timeline'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; -import { SourcererScopeName } from '../../store/sourcerer/model'; -import { useSourcererScope } from '../../containers/sourcerer'; -import { timelineSelectors } from '../../../timelines/store/timeline'; -import { stopPropagationAndPreventDefault } from '../../../../../timelines/public'; -import { TooltipWithKeyboardShortcut } from '../accessibility'; - -export const AdditionalContent = styled.div` - padding: 2px; -`; - -AdditionalContent.displayName = 'AdditionalContent'; - -const getAdditionalScreenReaderOnlyContext = ({ - field, - value, -}: { - field: string; - value?: string[] | string | null; -}): string => { - if (value == null) { - return field; - } - - return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`; -}; - -const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f'; -const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o'; -const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a'; -const SHOW_TOP_N_KEYBOARD_SHORTCUT = 't'; -const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; - -interface Props { - additionalContent?: React.ReactNode; - closePopOver?: () => void; - draggableId?: DraggableId; - field: string; - goGetTimelineId?: (args: boolean) => void; - onFilterAdded?: () => void; - ownFocus: boolean; - showTopN: boolean; - timelineId?: string | null; - toggleTopN: () => void; - value?: string[] | string | null; -} - -/** Returns a value for the `disabled` prop of `EuiFocusTrap` */ -const isFocusTrapDisabled = ({ - ownFocus, - showTopN, -}: { - ownFocus: boolean; - showTopN: boolean; -}): boolean => { - if (showTopN) { - return false; // we *always* want to trap focus when showing Top N - } - - return !ownFocus; -}; - -const DraggableWrapperHoverContentComponent: React.FC = ({ - additionalContent = null, - closePopOver, - draggableId, - field, - goGetTimelineId, - onFilterAdded, - ownFocus, - showTopN, - timelineId, - toggleTopN, - value, -}) => { - const kibana = useKibana(); - const { timelines } = kibana.services; - const { startDragToTimeline } = timelines.getUseAddToTimeline()({ - draggableId, - fieldName: field, - }); - const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ - kibana.services.data.query.filterManager, - ]); - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); - const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) => - getManageTimeline(state, timelineId ?? '') - ); - const defaultFocusedButtonRef = useRef(null); - const panelRef = useRef(null); - - const filterManager = useMemo( - () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup), - [timelineId, activeFilterMananager, filterManagerBackup] - ); - - // Regarding data from useManageTimeline: - // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `useWithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the alerts index - // to the index pattern. - const activeScope: SourcererScopeName = - timelineId === TimelineId.active - ? SourcererScopeName.timeline - : timelineId != null && - [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes( - timelineId as TimelineId - ) - ? SourcererScopeName.detections - : SourcererScopeName.default; - const { browserFields, indexPattern } = useSourcererScope(activeScope); - const handleStartDragToTimeline = useCallback(() => { - startDragToTimeline(); - if (closePopOver != null) { - closePopOver(); - } - }, [closePopOver, startDragToTimeline]); - - const filterForValue = useCallback(() => { - const filter = - value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); - const activeFilterManager = filterManager; - - if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); - if (closePopOver != null) { - closePopOver(); - } - if (onFilterAdded != null) { - onFilterAdded(); - } - } - }, [closePopOver, field, value, filterManager, onFilterAdded]); - - const filterOutValue = useCallback(() => { - const filter = - value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); - const activeFilterManager = filterManager; - - if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); - - if (closePopOver != null) { - closePopOver(); - } - if (onFilterAdded != null) { - onFilterAdded(); - } - } - }, [closePopOver, field, value, filterManager, onFilterAdded]); - - const isInit = useRef(true); - - useEffect(() => { - if (isInit.current && goGetTimelineId != null && timelineId == null) { - isInit.current = false; - goGetTimelineId(true); - } - }, [goGetTimelineId, timelineId]); - - useEffect(() => { - if (ownFocus) { - setTimeout(() => { - defaultFocusedButtonRef.current?.focus(); - }, 0); - } - }, [ownFocus]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (!ownFocus) { - return; - } - - switch (keyboardEvent.key) { - case FILTER_FOR_VALUE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - filterForValue(); - break; - case FILTER_OUT_VALUE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - filterOutValue(); - break; - case ADD_TO_TIMELINE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - handleStartDragToTimeline(); - break; - case SHOW_TOP_N_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - toggleTopN(); - break; - case COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - const copyToClipboardButton = panelRef.current?.querySelector( - `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` - ); - if (copyToClipboardButton != null) { - copyToClipboardButton.click(); - if (closePopOver != null) { - closePopOver(); - } - } - break; - case 'Enter': - break; - case 'Escape': - stopPropagationAndPreventDefault(keyboardEvent); - if (closePopOver != null) { - closePopOver(); - } - break; - default: - break; - } - }, - - [closePopOver, filterForValue, filterOutValue, handleStartDragToTimeline, ownFocus, toggleTopN] - ); - - return ( - - - -

{i18n.YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}

-
- - {additionalContent != null && {additionalContent}} - - {!showTopN && value != null && ( - - } - > - - - )} - - {!showTopN && value != null && ( - - } - > - - - )} - - {!showTopN && value != null && draggableId != null && ( - - } - > - - - )} - - <> - {allowTopN({ - browserField: getAllFieldsByName(browserFields)[field], - fieldName: field, - }) && ( - <> - {!showTopN && ( - - } - > - - - )} - - {showTopN && ( - - )} - - )} - - - {!showTopN && ( - - )} -
-
- ); -}; - -DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; - -export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); - -export const useGetTimelineId = function ( - elem: React.MutableRefObject, - getTimelineId: boolean = false -) { - const [timelineId, setTimelineId] = useState(null); - - useEffect(() => { - let startElem: Element | (Node & ParentNode) | null = elem.current; - if (startElem != null && getTimelineId) { - for (; startElem && startElem !== document; startElem = startElem.parentNode) { - const myElem: Element = startElem as Element; - if ( - myElem != null && - myElem.classList != null && - myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && - myElem.hasAttribute('data-timeline-id') - ) { - setTimelineId(myElem.getAttribute('data-timeline-id')); - break; - } - } - } - }, [elem, getTimelineId]); - - return timelineId; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx new file mode 100644 index 00000000000000..fcb547842aec4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; + +export const useGetTimelineId = function ( + elem: React.MutableRefObject, + getTimelineId: boolean = false +) { + const [timelineId, setTimelineId] = useState(null); + + useEffect(() => { + let startElem: Element | (Node & ParentNode) | null = elem.current; + if (startElem != null && getTimelineId) { + for (; startElem && startElem !== document; startElem = startElem.parentNode) { + const myElem: Element = startElem as Element; + if ( + myElem != null && + myElem.classList != null && + myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && + myElem.hasAttribute('data-timeline-id') + ) { + setTimelineId(myElem.getAttribute('data-timeline-id')); + break; + } + } + } + }, [elem, getTimelineId]); + + return timelineId; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap index 93608a181adffa..6b27cf5969f1aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap @@ -36,6 +36,7 @@ exports[`draggables rendering it renders the default DefaultDraggable 1`] = ` }, } } + isDraggable={true} render={[Function]} /> `; 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 df92da0c7d0563..6ac1746d77709b 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 @@ -20,6 +20,7 @@ import { Provider } from '../../../timelines/components/timeline/data_providers/ export interface DefaultDraggableType { id: string; + isDraggable?: boolean; field: string; value?: string | null; name?: string | null; @@ -79,6 +80,7 @@ Content.displayName = 'Content'; * that's only displayed when the specified value is non-`null`. * * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` + * @param isDraggable - optional prop to disable drag & drop and it will defaulted to true * @param field - the name of the field, e.g. `network.transport` * @param value - value of the field e.g. `tcp` * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data @@ -88,7 +90,17 @@ 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 }) => { + ({ + id, + isDraggable = true, + field, + value, + name, + children, + timelineId, + tooltipContent, + queryValue, + }) => { const dataProviderProp: DataProvider = useMemo( () => ({ and: [], @@ -125,6 +137,7 @@ export const DefaultDraggable = React.memo( return ( @@ -155,6 +168,7 @@ export type BadgeDraggableType = Omit & { * @param field - the name of the field, e.g. `network.transport` * @param value - value of the field e.g. `tcp` * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge + * @param isDraggable * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior @@ -168,6 +182,7 @@ const DraggableBadgeComponent: React.FC = ({ field, value, iconType, + isDraggable, name, color = 'hollow', children, @@ -177,6 +192,7 @@ const DraggableBadgeComponent: React.FC = ({ value != null ? ( = ({ key={key} contextId={key} eventId={eventId} + isDraggable={false} fieldName={fieldName || 'unknown'} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 80c014771ae68d..28a90e94c0ca41 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -245,7 +245,7 @@ describe('EventFieldsBrowser', () => { /> ); - expect(wrapper.find('[data-test-subj="draggable-content-@timestamp"]').at(0).text()).toEqual( + expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').at(0).text()).toEqual( 'Feb 28, 2019 @ 16:50:54.621' ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx index f5cf600e281ad1..67b1874eea0a00 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -6,11 +6,10 @@ */ import React, { useCallback, useState, useRef } from 'react'; -import { getDraggableId } from '@kbn/securitysolution-t-grid'; import { HoverActions } from '../../hover_actions'; import { useActionCellDataProvider } from './use_action_cell_data_provider'; import { EventFieldsData } from '../types'; -import { useGetTimelineId } from '../../drag_and_drop/draggable_wrapper_hover_content'; +import { useGetTimelineId } from '../../drag_and_drop/use_get_timeline_id_from_dom'; import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; import { BrowserField } from '../../../containers/source'; @@ -66,11 +65,10 @@ export const ActionCell: React.FC = React.memo( }); }, []); - const draggableIds = actionCellConfig?.idList.map((id) => getDraggableId(id)); return ( ({ + and: [], + enabled: true, + id: escapeDataProviderId(id), + name: field, + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value, + operator: IS_OPERATOR, + }, +}); + export const useActionCellDataProvider = ({ contextId, eventId, @@ -50,72 +66,90 @@ export const useActionCellDataProvider = ({ isObjectArray, linkValue, values, -}: UseActionCellDataProvider): { idList: string[]; stringValues: string[] } | null => { - if (values === null || values === undefined) return null; - - const stringifiedValues: string[] = []; - const arrayValues = Array.isArray(values) ? values : [values]; +}: UseActionCellDataProvider): { + stringValues: string[]; + dataProvider: DataProvider[]; +} | null => { + const cellData = useMemo(() => { + if (values === null || values === undefined) return null; + const arrayValues = Array.isArray(values) ? values : [values]; + return arrayValues.reduce<{ + stringValues: string[]; + dataProvider: DataProvider[]; + }>( + (memo, value, index) => { + let id: string = ''; + let valueAsString: string = isString(value) ? value : `${values}`; + const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}`; + if (fieldFromBrowserField == null) { + memo.stringValues.push(valueAsString); + return memo; + } - const idList: string[] = arrayValues.reduce((memo, value, index) => { - let id = null; - let valueAsString: string = isString(value) ? value : `${values}`; - if (fieldFromBrowserField == null) { - stringifiedValues.push(valueAsString); - return memo; - } - const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}-${eventId}-${field}-${value}`; - if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) { - stringifiedValues.push(valueAsString); - return memo; - } else if (fieldType === IP_FIELD_TYPE) { - id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`; - if (isString(value) && !isEmpty(value)) { - try { - const addresses = JSON.parse(value); - if (isArray(addresses)) { - valueAsString = addresses.join(','); + if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) { + memo.stringValues.push(valueAsString); + return memo; + } else if (fieldType === IP_FIELD_TYPE) { + id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`; + if (isString(value) && !isEmpty(value)) { + try { + const addresses = JSON.parse(value); + if (isArray(addresses)) { + valueAsString = addresses.join(','); + addresses.forEach((ip) => memo.dataProvider.push(getDataProvider(field, id, ip))); + } + } catch (_) { + // Default to keeping the existing string value + } + memo.stringValues.push(valueAsString); + return memo; } - } catch (_) { - // Default to keeping the existing string value + } else if (PORT_NAMES.some((portName) => field === portName)) { + id = `port-default-draggable-${appendedUniqueId}`; + } else if (field === EVENT_DURATION_FIELD_NAME) { + id = `duration-default-draggable-${appendedUniqueId}`; + } else if (field === HOST_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}`; + } else if (fieldFormat === BYTES_FORMAT) { + id = `bytes-default-draggable-${appendedUniqueId}`; + } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; + } else if (field === EVENT_MODULE_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else if (field === SIGNAL_STATUS_FIELD_NAME) { + id = `alert-details-value-default-draggable-${appendedUniqueId}`; + } else if (field === AGENT_STATUS_FIELD_NAME) { + const valueToUse = typeof value === 'string' ? value : ''; + id = `event-details-value-default-draggable-${appendedUniqueId}`; + valueAsString = valueToUse; + } else if ( + [ + RULE_REFERENCE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, + INDICATOR_REFERENCE, + ].includes(field) + ) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else { + id = `event-details-value-default-draggable-${appendedUniqueId}`; } - } - } else if (PORT_NAMES.some((portName) => field === portName)) { - id = `port-default-draggable-${appendedUniqueId}`; - } else if (field === EVENT_DURATION_FIELD_NAME) { - id = `duration-default-draggable-${appendedUniqueId}`; - } else if (field === HOST_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } else if (fieldFormat === BYTES_FORMAT) { - id = `bytes-default-draggable-${appendedUniqueId}`; - } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; - } else if (field === EVENT_MODULE_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else if (field === SIGNAL_STATUS_FIELD_NAME) { - id = `alert-details-value-default-draggable-${appendedUniqueId}`; - } else if (field === AGENT_STATUS_FIELD_NAME) { - const valueToUse = typeof value === 'string' ? value : ''; - id = `event-details-value-default-draggable-${appendedUniqueId}`; - valueAsString = valueToUse; - } else if ( - [ - RULE_REFERENCE_FIELD_NAME, - REFERENCE_URL_FIELD_NAME, - EVENT_URL_FIELD_NAME, - INDICATOR_REFERENCE, - ].includes(field) - ) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } - stringifiedValues.push(valueAsString); - memo.push(escapeDataProviderId(id)); - return memo; - }, [] as string[]); - - return { - idList, - stringValues: stringifiedValues, - }; + memo.stringValues.push(valueAsString); + memo.dataProvider.push(getDataProvider(field, id, value)); + return memo; + }, + { stringValues: [], dataProvider: [] } + ); + }, [ + contextId, + eventId, + field, + fieldFormat, + fieldFromBrowserField, + fieldType, + isObjectArray, + linkValue, + values, + ]); + return cellData; }; diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx index 31bdf78626e7cb..a1ba33f30cc55c 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx @@ -6,16 +6,17 @@ */ import { EuiFocusTrap, EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { getAllFieldsByName } from '../../containers/source'; -import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; +import { isEmpty } from 'lodash'; + import { useKibana } from '../../lib/kibana'; +import { getAllFieldsByName } from '../../containers/source'; import { allowTopN } from './utils'; import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { ColumnHeaderOptions, TimelineId } from '../../../../common/types/timeline'; +import { ColumnHeaderOptions, DataProvider, TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { timelineSelectors } from '../../../timelines/store/timeline'; @@ -38,43 +39,51 @@ export const AdditionalContent = styled.div` AdditionalContent.displayName = 'AdditionalContent'; -const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` +const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean; $showOwnFocus: boolean }>` padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; display: flex; - &:focus-within { - .timelines__hoverActionButton, - .securitySolution__hoverActionButton { - opacity: 1; + ${(props) => + props.$showOwnFocus + ? ` + &:focus-within { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } } - } - &:hover { - .timelines__hoverActionButton, - .securitySolution__hoverActionButton { - opacity: 1; + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } } - } .timelines__hoverActionButton, .securitySolution__hoverActionButton { - opacity: ${(props) => (props.$showTopN ? 1 : 0)}; + opacity: ${props.$showTopN ? 1 : 0}; - &:focus { - opacity: 1; + &:focus { + opacity: 1; + } } - } + ` + : ''} `; interface Props { additionalContent?: React.ReactNode; + closePopOver?: () => void; + dataProvider?: DataProvider | DataProvider[]; dataType?: string; - draggableIds?: DraggableId[]; + draggableId?: DraggableId; field: string; goGetTimelineId?: (args: boolean) => void; isObjectArray: boolean; onFilterAdded?: () => void; ownFocus: boolean; + showOwnFocus?: boolean; showTopN: boolean; timelineId?: string | null; toggleColumn?: (column: ColumnHeaderOptions) => void; @@ -100,13 +109,15 @@ const isFocusTrapDisabled = ({ export const HoverActions: React.FC = React.memo( ({ additionalContent = null, + dataProvider, dataType, - draggableIds, + draggableId, field, goGetTimelineId, isObjectArray, onFilterAdded, ownFocus, + showOwnFocus = true, showTopN, timelineId, toggleColumn, @@ -117,29 +128,13 @@ export const HoverActions: React.FC = React.memo( const { timelines } = kibana.services; // Common actions used by the alert table and alert flyout const { - addToTimeline: { - AddToTimelineButton, - keyboardShortcut: addToTimelineKeyboardShortcut, - useGetHandleStartDragToTimeline, - }, - columnToggle: { - ColumnToggleButton, - columnToggleFn, - keyboardShortcut: columnToggleKeyboardShortcut, - }, - copy: { CopyButton, keyboardShortcut: copyKeyboardShortcut }, - filterForValue: { - FilterForValueButton, - filterForValueFn, - keyboardShortcut: filterForValueKeyboardShortcut, - }, - filterOutValue: { - FilterOutValueButton, - filterOutValueFn, - keyboardShortcut: filterOutValueKeyboardShortcut, - }, + getAddToTimelineButton, + getColumnToggleButton, + getCopyButton, + getFilterForValueButton, + getFilterOutValueButton, } = timelines.getHoverActions(); - + const [stKeyboardEvent, setStKeyboardEvent] = useState(); const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); @@ -169,30 +164,8 @@ export const HoverActions: React.FC = React.memo( : SourcererScopeName.default; const { browserFields } = useSourcererScope(activeScope); - const handleStartDragToTimeline = (() => { - const handleStartDragToTimelineFns = draggableIds?.map((draggableId) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useGetHandleStartDragToTimeline({ draggableId, field }); - }); - return () => handleStartDragToTimelineFns?.forEach((dragFn) => dragFn()); - })(); - - const handleFilterForValue = useCallback(() => { - filterForValueFn({ field, value: values, filterManager, onFilterAdded }); - }, [filterForValueFn, field, values, filterManager, onFilterAdded]); - - const handleFilterOutValue = useCallback(() => { - filterOutValueFn({ field, value: values, filterManager, onFilterAdded }); - }, [filterOutValueFn, field, values, filterManager, onFilterAdded]); - - const handleToggleColumn = useCallback( - () => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null), - [columnToggleFn, field, toggleColumn] - ); - const isInit = useRef(true); const defaultFocusedButtonRef = useRef(null); - const panelRef = useRef(null); useEffect(() => { if (isInit.current && goGetTimelineId != null && timelineId == null) { @@ -215,31 +188,6 @@ export const HoverActions: React.FC = React.memo( return; } switch (keyboardEvent.key) { - case addToTimelineKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleStartDragToTimeline(); - break; - case columnToggleKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleToggleColumn(); - break; - case copyKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - const copyToClipboardButton = panelRef.current?.querySelector( - `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` - ); - if (copyToClipboardButton != null) { - copyToClipboardButton.click(); - } - break; - case filterForValueKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleFilterForValue(); - break; - case filterOutValueKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleFilterOutValue(); - break; case SHOW_TOP_N_KEYBOARD_SHORTCUT: stopPropagationAndPreventDefault(keyboardEvent); toggleTopN(); @@ -250,33 +198,26 @@ export const HoverActions: React.FC = React.memo( stopPropagationAndPreventDefault(keyboardEvent); break; default: + setStKeyboardEvent(keyboardEvent); break; } }, - [ - addToTimelineKeyboardShortcut, - columnToggleKeyboardShortcut, - copyKeyboardShortcut, - filterForValueKeyboardShortcut, - filterOutValueKeyboardShortcut, - handleFilterForValue, - handleFilterOutValue, - handleStartDragToTimeline, - handleToggleColumn, - ownFocus, - toggleTopN, - ] + [ownFocus, toggleTopN] ); const showFilters = values != null; return ( - - +

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}

@@ -286,46 +227,58 @@ export const HoverActions: React.FC = React.memo( {showFilters && ( <> - - +
+ {getFilterForValueButton({ + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent: stKeyboardEvent, + onFilterAdded, + ownFocus, + showTooltip: true, + value: values, + })} +
+
+ {getFilterOutValueButton({ + field, + filterManager, + keyboardEvent: stKeyboardEvent, + onFilterAdded, + ownFocus, + showTooltip: true, + value: values, + })} +
)} {toggleColumn && ( - +
+ {getColumnToggleButton({ + field, + isDisabled: isObjectArray && dataType !== 'geo_point', + isObjectArray, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + toggleColumn, + value: values, + })} +
)} - {showFilters && draggableIds != null && ( - + {showFilters && (draggableId != null || !isEmpty(dataProvider)) && ( +
+ {getAddToTimelineButton({ + dataProvider, + draggableId, + field, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + value: values, + })} +
)} {allowTopN({ browserField: getAllFieldsByName(browserFields)[field], @@ -342,18 +295,20 @@ export const HoverActions: React.FC = React.memo( value={values} /> )} - {showFilters && ( - + {field != null && ( +
+ {getCopyButton({ + field, + isHoverAction: true, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + value: values, + })} +
)} -
-
+ + ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx new file mode 100644 index 00000000000000..373f944b70a815 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState, useRef } from 'react'; +import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import { HoverActions } from '.'; + +import { DataProvider } from '../../../../common/types'; +import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper'; +import { getDraggableId } from '../drag_and_drop/helpers'; +import { useGetTimelineId } from '../drag_and_drop/use_get_timeline_id_from_dom'; + +const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => { + const links = draggableElement?.querySelectorAll('.euiLink') ?? []; + return links.length > 0; +}; + +type RenderFunctionProp = ( + props: DataProvider, + provided: DraggableProvided | null, + state: DraggableStateSnapshot +) => React.ReactNode; + +interface Props { + dataProvider: DataProvider; + disabled?: boolean; + isDraggable?: boolean; + inline?: boolean; + render: RenderFunctionProp; + timelineId?: string; + truncate?: boolean; + onFilterAdded?: () => void; +} + +export const useHoverActions = ({ + dataProvider, + isDraggable, + onFilterAdded, + render, + timelineId, +}: Props) => { + const containerRef = useRef(null); + const keyboardHandlerRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); + const [showTopN, setShowTopN] = useState(false); + const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(containerRef, goGetTimelineId); + + const handleClosePopOverTrigger = useCallback(() => { + setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); + setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { + if (prevHoverActionsOwnFocus) { + setTimeout(() => { + keyboardHandlerRef.current?.focus(); + }, 0); + } + return false; // always give up ownership + }); + + setTimeout(() => { + setHoverActionsOwnFocus(false); + }, 0); // invoked on the next tick, because we want to restore focus first + }, [keyboardHandlerRef]); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + if (newShowTopN === false) { + handleClosePopOverTrigger(); + } + return newShowTopN; + }); + }, [handleClosePopOverTrigger]); + + const hoverContent = useMemo(() => { + // display links as additional content in the hover menu to enable keyboard + // navigation of links (when the draggable contains them): + const additionalContent = + hoverActionsOwnFocus && !showTopN && draggableContainsLinks(containerRef.current) ? ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + ) : null; + + return ( + + ); + }, [ + dataProvider, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + isDraggable, + onFilterAdded, + render, + showTopN, + timelineId, + timelineIdFind, + toggleTopN, + ]); + + const setContainerRef = useCallback((e: HTMLDivElement) => { + containerRef.current = e; + }, []); + + const onFocus = useCallback(() => { + if (!hoverActionsOwnFocus) { + keyboardHandlerRef.current?.focus(); + } + }, [hoverActionsOwnFocus, keyboardHandlerRef]); + + const onCloseRequested = useCallback(() => { + setShowTopN(false); + + if (hoverActionsOwnFocus) { + setHoverActionsOwnFocus(false); + + setTimeout(() => { + onFocus(); // return focus to this draggable on the next tick, because we owned focus + }, 0); + } + }, [onFocus, hoverActionsOwnFocus]); + + const openPopover = useCallback(() => { + setHoverActionsOwnFocus(true); + }, []); + + return useMemo( + () => ({ + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + keyboardHandlerRef, + onCloseRequested, + onFocus, + openPopover, + setContainerRef, + showTopN, + }), + [ + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + onCloseRequested, + onFocus, + openPopover, + setContainerRef, + showTopN, + ] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 2ecda8482e3400..45883019b9ff83 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -70,67 +70,6 @@ describe('get_anomalies_host_table_columns', () => { expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(false); }); - test('on host page, we should escape the draggable id', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.page, - startDate, - endDate, - interval, - narrowDateRange - ); - const column = columns.find((col) => col.name === i18n.SCORE) as Columns< - string, - AnomaliesByHost - >; - const anomaly: AnomaliesByHost = { - hostName: 'host.name', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - influencers: [], - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render('', anomaly)}); - expect( - wrapper - .find( - '[draggableId="draggableId.content.anomalies-host-table-severity-host_name-entity-name-1-entity-value-1-100-job-1"]' - ) - .first() - .exists() - ).toBe(true); - } else { - expect(column).not.toBe(null); - } - }); - test('on host page, undefined influencers should turn into an empty column string', () => { const columns = getAnomaliesHostTableColumnsCurated( HostsType.page, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 48c2ec3ee38d81..817205ce22808d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -43,62 +43,6 @@ describe('get_anomalies_network_table_columns', () => { expect(columns.some((col) => col.name === i18n.NETWORK_NAME)).toEqual(false); }); - test('on network page, we should escape the draggable id', () => { - const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate); - const column = columns.find((col) => col.name === i18n.SCORE) as Columns< - string, - AnomaliesByNetwork - >; - const anomaly: AnomaliesByNetwork = { - type: 'source.ip', - ip: '127.0.0.1', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - influencers: [], - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render('', anomaly)}); - expect( - wrapper - .find( - '[draggableId="draggableId.content.anomalies-network-table-severity-127_0_0_1-entity-name-1-entity-value-1-100-job-1"]' - ) - .first() - .exists() - ).toBe(true); - } else { - expect(column).not.toBe(null); - } - }); - test('on network page, undefined influencers should turn into an empty column string', () => { const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate); const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns< diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index c122138f9547a5..10e4538c802ad8 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -55,7 +55,7 @@ describe('Table Helpers', () => { displayCount: 0, }); const wrapper = mount({rowItem}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( '(Empty String)' ); }); @@ -81,7 +81,7 @@ describe('Table Helpers', () => { render: renderer, }); const wrapper = mount({rowItem}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( 'Hi item1 renderer' ); }); @@ -116,7 +116,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( '(Empty String)' ); }); @@ -163,7 +163,7 @@ describe('Table Helpers', () => { displayCount: 2, }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').hostNodes().length).toBe(2); + expect(wrapper.find('[data-test-subj="withHoverActionsButton"]').hostNodes().length).toBe(2); }); test('it uses custom renderer', () => { @@ -175,7 +175,7 @@ describe('Table Helpers', () => { render: renderer, }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( 'Hi item1 renderer' ); }); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index b9bbf7afd36265..adc06468d9a024 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -62,6 +62,7 @@ export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => return [...acc, value]; } }, []); + export const convertToCamelCase = (snakeCase: T): U => Object.entries(snakeCase).reduce((acc, [key, value]) => { if (isArray(value)) { @@ -73,6 +74,7 @@ export const convertToCamelCase = (snakeCase: T): U => } return acc; }, {} as U); + export const useCurrentUser = (): AuthenticatedElasticUser | null => { const isMounted = useRef(false); const [user, setUser] = useState(null); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 2206960f6bcd3a..55154def55b501 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -21,7 +21,7 @@ import { ActionVariables, } from '../../../../../../triggers_actions_ui/public'; import { AlertAction } from '../../../../../../alerting/common'; -import { useKibana } from '../../../../common/lib/kibana'; +import { convertArrayToCamelCase, useKibana } from '../../../../common/lib/kibana'; import { FORM_ERRORS_TITLE } from './translations'; interface Props { @@ -137,7 +137,7 @@ export const RuleActionsField: React.FC = ({ useEffect(() => { (async function () { - const actionTypes = await loadActionTypes({ http }); + const actionTypes = convertArrayToCamelCase(await loadActionTypes({ http })) as ActionType[]; const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction); setSupportedActionTypes(supportedTypes); })(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx index c19e5c26bdc942..6fd90adfc0e06b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx @@ -6,11 +6,11 @@ */ import { useEffect, useRef, useState } from 'react'; -import { ACTION_URL } from '../../../../../../cases/common'; -import { KibanaServices } from '../../../../common/lib/kibana'; +import { getAllConnectorsUrl, getCreateConnectorUrl } from '../../../../../../cases/common'; +import { convertArrayToCamelCase, KibanaServices } from '../../../../common/lib/kibana'; interface CaseAction { - actionTypeId: string; + connectorTypeId: string; id: string; isPreconfigured: boolean; name: string; @@ -28,15 +28,18 @@ export const useManageCaseAction = () => { const abortCtrl = new AbortController(); const fetchActions = async () => { try { - const actions = await KibanaServices.get().http.fetch(ACTION_URL, { - method: 'GET', - signal: abortCtrl.signal, - }); - if (!actions.some((a) => a.actionTypeId === '.case' && a.name === CASE_ACTION_NAME)) { - await KibanaServices.get().http.post(`${ACTION_URL}/action`, { + const actions = convertArrayToCamelCase( + await KibanaServices.get().http.fetch(getAllConnectorsUrl(), { + method: 'GET', + signal: abortCtrl.signal, + }) + ) as CaseAction[]; + + if (!actions.some((a) => a.connectorTypeId === '.case' && a.name === CASE_ACTION_NAME)) { + await KibanaServices.get().http.post(getCreateConnectorUrl(), { method: 'POST', body: JSON.stringify({ - actionTypeId: '.case', + connector_type_id: '.case', config: {}, name: CASE_ACTION_NAME, secrets: {}, diff --git a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx index 7c6bb50378d9c7..d87756cb9bbab0 100644 --- a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx @@ -60,13 +60,15 @@ export const DirectionBadge = React.memo<{ contextId: string; direction?: string | null; eventId: string; -}>(({ contextId, eventId, direction }) => ( + isDraggable?: boolean; +}>(({ contextId, eventId, direction, isDraggable }) => ( )); diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx index a08b8003f142c0..2fa3e988784ca0 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx @@ -22,13 +22,15 @@ export const Ip = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable?: boolean; value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( +}>(({ contextId, eventId, fieldName, isDraggable, value }) => ( diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.tsx index df288c1abfb06c..4afd9bc7b892a5 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.tsx @@ -29,7 +29,7 @@ export const Port = React.memo<{ contextId: string; eventId: string; fieldName: string; - isDraggable: boolean; + isDraggable?: boolean; value: string | undefined | null; }>(({ contextId, eventId, fieldName, isDraggable, value }) => isDraggable ? ( @@ -37,6 +37,7 @@ export const Port = React.memo<{ data-test-subj="port" field={fieldName} id={`port-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`} + isDraggable={isDraggable} tooltipContent={fieldName} value={value} > diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx index 1f6111cd0bb079..65bd3bf1ec154c 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx @@ -73,8 +73,9 @@ const GeoFieldValues = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable?: boolean; values?: string[] | null; -}>(({ contextId, eventId, fieldName, values }) => +}>(({ contextId, eventId, fieldName, isDraggable, values }) => values != null ? ( <> {uniq(values).map((value) => ( @@ -92,6 +93,7 @@ const GeoFieldValues = React.memo<{ data-test-subj={fieldName} field={fieldName} id={`geo-field-values-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`} + isDraggable={isDraggable} tooltipContent={fieldName} value={value} /> @@ -114,7 +116,7 @@ GeoFieldValues.displayName = 'GeoFieldValues'; * - `source|destination.geo.city_name` */ export const GeoFields = React.memo((props) => { - const { contextId, eventId, type } = props; + const { contextId, eventId, isDraggable, type } = props; const propNameToFieldName = getGeoFieldPropNameToFieldNameMap(type); return ( @@ -124,6 +126,7 @@ export const GeoFields = React.memo((props) => { contextId={contextId} eventId={eventId} fieldName={geo.fieldName} + isDraggable={isDraggable} key={geo.fieldName} values={get(geo.prop, props)} /> diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx index 57e302d2911fa3..d7bcf9f6c52971 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx @@ -36,6 +36,7 @@ export const SourceDestination = React.memo( destinationPackets, destinationPort, eventId, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -59,8 +60,9 @@ export const SourceDestination = React.memo( packets={networkPackets} communityId={networkCommunityId} contextId={contextId} - eventId={eventId} direction={networkDirection} + eventId={eventId} + isDraggable={isDraggable} protocol={networkProtocol} transport={transport} /> @@ -79,6 +81,7 @@ export const SourceDestination = React.memo( destinationPackets={destinationPackets} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} sourceBytes={sourceBytes} sourceGeoContinentName={sourceGeoContinentName} sourceGeoCountryName={sourceGeoCountryName} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx index 17b55c4229fcc7..e99aecbc535e76 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx @@ -25,9 +25,10 @@ IpPortSeparator.displayName = 'IpPortSeparator'; const PortWithSeparator = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; port?: string | null; portFieldName: string; -}>(({ contextId, eventId, port, portFieldName }) => { +}>(({ contextId, eventId, isDraggable, port, portFieldName }) => { return port != null ? ( @@ -39,7 +40,7 @@ const PortWithSeparator = React.memo<{ data-test-subj="port" eventId={eventId} fieldName={portFieldName} - isDraggable={true} + isDraggable={isDraggable} value={port} /> @@ -58,9 +59,10 @@ export const IpWithPort = React.memo<{ eventId: string; ip?: string | null; ipFieldName: string; + isDraggable?: boolean; port?: string | null; portFieldName: string; -}>(({ contextId, eventId, ip, ipFieldName, port, portFieldName }) => ( +}>(({ contextId, eventId, ip, ipFieldName, isDraggable, port, portFieldName }) => ( @@ -75,6 +78,7 @@ export const IpWithPort = React.memo<{ diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx index c1b454892fddf9..88bfd19b7066e1 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx @@ -45,97 +45,120 @@ export const Network = React.memo<{ contextId: string; direction?: string[] | null; eventId: string; + isDraggable?: boolean; packets?: string[] | null; protocol?: string[] | null; transport?: string[] | null; -}>(({ bytes, communityId, contextId, direction, eventId, packets, protocol, transport }) => ( - - {direction != null - ? uniq(direction).map((dir) => ( - - - - )) - : null} +}>( + ({ + bytes, + communityId, + contextId, + direction, + eventId, + isDraggable, + packets, + protocol, + transport, + }) => ( + + {direction != null + ? uniq(direction).map((dir) => ( + + + + )) + : null} + + {protocol != null + ? uniq(protocol).map((proto) => ( + + + + )) + : null} - {protocol != null - ? uniq(protocol).map((proto) => ( - - - - )) - : null} + {bytes != null + ? uniq(bytes).map((b) => + !isNaN(Number(b)) ? ( + + + + + + + + + + ) : null + ) + : null} - {bytes != null - ? uniq(bytes).map((b) => - !isNaN(Number(b)) ? ( - + {packets != null + ? uniq(packets).map((p) => ( + - - - + {`${p} ${i18n.PACKETS}`} - ) : null - ) - : null} + )) + : null} - {packets != null - ? uniq(packets).map((p) => ( - - - - {`${p} ${i18n.PACKETS}`} - - - - )) - : null} - - {transport != null - ? uniq(transport).map((trans) => ( - - - - )) - : null} + {transport != null + ? uniq(transport).map((trans) => ( + + + + )) + : null} - {communityId != null - ? uniq(communityId).map((trans) => ( - - - - )) - : null} - -)); + {communityId != null + ? uniq(communityId).map((trans) => ( + + + + )) + : null} + + ) +); Network.displayName = 'Network'; 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 ff9edff39b3ad3..6858520340aae7 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 @@ -56,10 +56,11 @@ Data.displayName = 'Data'; const SourceArrow = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; sourceBytes: string | undefined; sourceBytesPercent: number | undefined; sourcePackets: string | undefined; -}>(({ contextId, eventId, sourceBytes, sourceBytesPercent, sourcePackets }) => { +}>(({ contextId, eventId, isDraggable, sourceBytes, sourceBytesPercent, sourcePackets }) => { const sourceArrowHeight = sourceBytesPercent != null ? getArrowHeightFromPercent(sourceBytesPercent) @@ -76,6 +77,7 @@ const SourceArrow = React.memo<{ @@ -101,6 +103,7 @@ const SourceArrow = React.memo<{ @@ -129,73 +132,85 @@ SourceArrow.displayName = 'SourceArrow'; */ const DestinationArrow = React.memo<{ contextId: string; - eventId: string; destinationBytes: string | undefined; destinationBytesPercent: number | undefined; destinationPackets: string | undefined; -}>(({ contextId, eventId, destinationBytes, destinationBytesPercent, destinationPackets }) => { - const destinationArrowHeight = - destinationBytesPercent != null - ? getArrowHeightFromPercent(destinationBytesPercent) - : DEFAULT_ARROW_HEIGHT; + eventId: string; + isDraggable?: boolean; +}>( + ({ + contextId, + destinationBytes, + destinationBytesPercent, + destinationPackets, + eventId, + isDraggable, + }) => { + const destinationArrowHeight = + destinationBytesPercent != null + ? getArrowHeightFromPercent(destinationBytesPercent) + : DEFAULT_ARROW_HEIGHT; + + return ( + + + + - return ( - - - - + + + - - - + {destinationBytes != null && !isNaN(Number(destinationBytes)) ? ( + + + + {destinationBytesPercent != null ? ( + + {`(${numeral(destinationBytesPercent).format('0.00')}%)`} + + ) : null} + + + + + + + ) : null} - {destinationBytes != null && !isNaN(Number(destinationBytes)) ? ( - - - {destinationBytesPercent != null ? ( - - {`(${numeral(destinationBytesPercent).format('0.00')}%)`} - - ) : null} - - - - - + - ) : null} - - - + {destinationPackets != null && !isNaN(Number(destinationPackets)) ? ( + + + + {`${numeral(destinationPackets).format( + '0,0' + )} ${i18n.PACKETS}`} + + + + ) : null} - {destinationPackets != null && !isNaN(Number(destinationPackets)) ? ( - - - {`${numeral(destinationPackets).format( - '0,0' - )} ${i18n.PACKETS}`} - - + - ) : null} - - - - - - ); -}); + + ); + } +); DestinationArrow.displayName = 'DestinationArrow'; @@ -208,67 +223,79 @@ export const SourceDestinationArrows = React.memo<{ destinationBytes?: string[] | null; destinationPackets?: string[] | null; eventId: string; + isDraggable?: boolean; sourceBytes?: string[] | null; sourcePackets?: string[] | null; -}>(({ contextId, destinationBytes, destinationPackets, eventId, sourceBytes, sourcePackets }) => { - const maybeSourceBytes = - sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined; - - const maybeSourcePackets = - sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined; - - const maybeDestinationBytes = - destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined; - - const maybeDestinationPackets = - destinationPackets != null && hasOneValue(destinationPackets) - ? destinationPackets[0] - : undefined; - - const maybeSourceBytesPercent = - maybeSourceBytes != null && maybeDestinationBytes != null - ? getPercent({ - numerator: Number(maybeSourceBytes), - denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes), - }) - : undefined; - - const maybeDestinationBytesPercent = - maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined; - - return ( - - {maybeSourceBytes != null ? ( - - - - ) : null} - - {maybeDestinationBytes != null ? ( - - - - ) : null} - - ); -}); +}>( + ({ + contextId, + destinationBytes, + destinationPackets, + eventId, + isDraggable, + sourceBytes, + sourcePackets, + }) => { + const maybeSourceBytes = + sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined; + + const maybeSourcePackets = + sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined; + + const maybeDestinationBytes = + destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined; + + const maybeDestinationPackets = + destinationPackets != null && hasOneValue(destinationPackets) + ? destinationPackets[0] + : undefined; + + const maybeSourceBytesPercent = + maybeSourceBytes != null && maybeDestinationBytes != null + ? getPercent({ + numerator: Number(maybeSourceBytes), + denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes), + }) + : undefined; + + const maybeDestinationBytesPercent = + maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined; + + return ( + + {maybeSourceBytes != null ? ( + + + + ) : null} + {maybeDestinationBytes != null ? ( + + + + ) : null} + + ); + } +); SourceDestinationArrows.displayName = 'SourceDestinationArrows'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index 91f7ea3d7ac7a5..824b9fd11f2429 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -958,6 +958,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -979,7 +980,6 @@ describe('SourceDestinationIp', () => { /> ); - expect( removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() @@ -1011,6 +1011,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1064,6 +1065,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1118,6 +1120,7 @@ describe('SourceDestinationIp', () => { destinationIp={undefined} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1271,6 +1274,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx index db9773789bf549..31bae6880fcbe4 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx @@ -88,54 +88,67 @@ const IpAdressesWithPorts = React.memo<{ destinationIp?: string[] | null; destinationPort?: Array | null; eventId: string; + isDraggable?: boolean; sourceIp?: string[] | null; sourcePort?: Array | null; type: SourceDestinationType; -}>(({ contextId, destinationIp, destinationPort, eventId, sourceIp, sourcePort, type }) => { - const ip = type === 'source' ? sourceIp : destinationIp; - const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME; - const port = type === 'source' ? sourcePort : destinationPort; - const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME; - - if (ip == null) { - return null; // if ip is not populated as an array, ports will be ignored +}>( + ({ + contextId, + destinationIp, + destinationPort, + eventId, + isDraggable, + sourceIp, + sourcePort, + type, + }) => { + const ip = type === 'source' ? sourceIp : destinationIp; + const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME; + const port = type === 'source' ? sourcePort : destinationPort; + const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME; + + if (ip == null) { + return null; // if ip is not populated as an array, ports will be ignored + } + + // IMPORTANT: The ip and port arrays are parallel arrays; the port at + // index `i` corresponds with the ip address at index `i`. We must + // preserve the relationships between the parallel arrays: + const ipPortPairs: IpPortPair[] = + port != null && ip.length === port.length + ? ip.map((address, i) => ({ + ip: address, + port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array + })) + : ip.map((address) => ({ + ip: address, + port: null, // drop the port, because the length of the parallel ip and port arrays is different + })); + + return ( + + {uniqWith(deepEqual, ipPortPairs).map( + (ipPortPair) => + ipPortPair.ip != null && ( + + + + ) + )} + + ); } - - // IMPORTANT: The ip and port arrays are parallel arrays; the port at - // index `i` corresponds with the ip address at index `i`. We must - // preserve the relationships between the parallel arrays: - const ipPortPairs: IpPortPair[] = - port != null && ip.length === port.length - ? ip.map((address, i) => ({ - ip: address, - port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array - })) - : ip.map((address) => ({ - ip: address, - port: null, // drop the port, because the length of the parallel ip and port arrays is different - })); - - return ( - - {uniqWith(deepEqual, ipPortPairs).map( - (ipPortPair) => - ipPortPair.ip != null && ( - - - - ) - )} - - ); -}); +); IpAdressesWithPorts.displayName = 'IpAdressesWithPorts'; @@ -159,6 +172,7 @@ export const SourceDestinationIp = React.memo( destinationIp, destinationPort, eventId, + isDraggable, sourceGeoContinentName, sourceGeoCountryName, sourceGeoCountryIsoCode, @@ -189,6 +203,7 @@ export const SourceDestinationIp = React.memo( destinationIp={destinationIp} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} sourceIp={sourceIp} sourcePort={sourcePort} type={type} @@ -202,7 +217,7 @@ export const SourceDestinationIp = React.memo( data-test-subj="port" eventId={eventId} fieldName={`${type}.port`} - isDraggable={true} + isDraggable={isDraggable} value={port} /> @@ -219,6 +234,7 @@ export const SourceDestinationIp = React.memo( destinationGeoRegionName={destinationGeoRegionName} destinationGeoCityName={destinationGeoCityName} eventId={eventId} + isDraggable={isDraggable} sourceGeoContinentName={sourceGeoContinentName} sourceGeoCountryName={sourceGeoCountryName} sourceGeoCountryIsoCode={sourceGeoCountryIsoCode} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx index 3d6189118ecb08..a010d674291bab 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx @@ -32,6 +32,7 @@ export const SourceDestinationWithArrows = React.memo @@ -85,6 +88,7 @@ export const SourceDestinationWithArrows = React.memo | null; eventId: string; + isDraggable?: boolean; sourceGeoContinentName?: string[] | null; sourceGeoCountryName?: string[] | null; sourceGeoCountryIsoCode?: string[] | null; @@ -85,6 +88,7 @@ export interface SourceDestinationWithArrowsProps { destinationPackets?: string[] | null; destinationPort?: string[] | null; eventId: string; + isDraggable?: boolean; sourceBytes?: string[] | null; sourceGeoContinentName?: string[] | null; sourceGeoCountryName?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx index 29775067478a59..296faf208ac91e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx @@ -40,8 +40,9 @@ export const CertificateFingerprint = React.memo<{ certificateType: CertificateType; contextId: string; fieldName: string; + isDraggable?: boolean; value?: string | null; -}>(({ eventId, certificateType, contextId, fieldName, value }) => { +}>(({ eventId, certificateType, contextId, fieldName, isDraggable, value }) => { return ( {fieldName} diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx index 421ba5941eaefc..7500fdb122fae5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx @@ -26,6 +26,7 @@ export const Duration = React.memo<{ isDraggable ? ( @@ -24,6 +25,7 @@ exports[`Field Renderers #autonomousSystemRenderer it renders correctly against @@ -58,6 +60,7 @@ exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1 }, } } + isDraggable={false} render={[Function]} /> `; @@ -79,6 +82,7 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot }, } } + isDraggable={false} render={[Function]} /> `; @@ -94,6 +98,7 @@ exports[`Field Renderers #locationRenderer it renders correctly against snapshot @@ -84,6 +85,7 @@ export const autonomousSystemRenderer = ( id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }${flowTarget}.as.organization.name`} + isDraggable={false} field={`${flowTarget}.as.organization.name`} value={as.organization.name} /> @@ -94,6 +96,7 @@ export const autonomousSystemRenderer = ( id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }${flowTarget}.as.number`} + isDraggable={false} field={`${flowTarget}.as.number`} value={`${as.number}`} /> @@ -123,6 +126,7 @@ export const hostIdRenderer = ({ id={`host-id-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }host-id`} + isDraggable={false} field="host.id" value={host.id[0]} > @@ -154,6 +158,7 @@ export const hostNameRenderer = ( id={`host-name-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }host-name`} + isDraggable={false} field={'host.name'} value={host.name[0]} > @@ -204,7 +209,7 @@ export const DefaultFieldRendererComponent: React.FC )} {typeof rowItem === 'string' && ( - + {render ? render(rowItem) : rowItem} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 5014a198e8bd5d..5acc0ef9aa46b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -59,11 +59,11 @@ describe('FieldName', () => { ); await waitFor(() => { - wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); wrapper.update(); jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx index 2e76e43227506b..1e081d249cc00b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx @@ -11,11 +11,9 @@ import styled from 'styled-components'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { - DraggableWrapperHoverContent, - useGetTimelineId, -} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; +import { useGetTimelineId } from '../../../common/components/drag_and_drop/use_get_timeline_id_from_dom'; import { ColumnHeaderOptions } from '../../../../common'; +import { HoverActions } from '../../../common/components/hover_actions'; /** * The name of a (draggable) field @@ -112,9 +110,10 @@ export const FieldName = React.memo<{ const hoverContent = useMemo( () => ( - = ({ return ( (({ contextId, eventId, fieldName, value }) => ( +}>(({ contextId, eventId, fieldName, isDraggable, value }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx index 16ea48890778e7..328d3105240701 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx @@ -23,6 +23,7 @@ import { JA3_HASH_FIELD_NAME, Ja3Fingerprint } from '../../ja3_fingerprint'; export const Fingerprints = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; tlsClientCertificateFingerprintSha1?: string[] | null; tlsFingerprintsJa3Hash?: string[] | null; tlsServerCertificateFingerprintSha1?: string[] | null; @@ -30,6 +31,7 @@ export const Fingerprints = React.memo<{ ({ contextId, eventId, + isDraggable, tlsClientCertificateFingerprintSha1, tlsFingerprintsJa3Hash, tlsServerCertificateFingerprintSha1, @@ -48,6 +50,7 @@ export const Fingerprints = React.memo<{ eventId={eventId} fieldName={JA3_HASH_FIELD_NAME} contextId={contextId} + isDraggable={isDraggable} value={ja3} /> @@ -61,6 +64,7 @@ export const Fingerprints = React.memo<{ certificateType="client" contextId={contextId} fieldName={TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME} + isDraggable={isDraggable} value={clientCert} /> @@ -74,6 +78,7 @@ export const Fingerprints = React.memo<{ certificateType="server" contextId={contextId} fieldName={TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME} + isDraggable={isDraggable} value={serverCert} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx index 05bfe56d1df429..a755aa54fca7b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx @@ -37,6 +37,7 @@ export const Netflow = React.memo( eventId, eventEnd, eventStart, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -82,6 +83,7 @@ export const Netflow = React.memo( eventId={eventId} eventEnd={eventEnd} eventStart={eventStart} + isDraggable={isDraggable} networkBytes={networkBytes} networkCommunityId={networkCommunityId} networkDirection={networkDirection} @@ -105,6 +107,7 @@ export const Netflow = React.memo( (({ contextId, eventDuration, eventId, eventEnd, eventStart }) => ( + isDraggable?: boolean; +}>(({ contextId, eventDuration, eventId, eventEnd, eventStart, isDraggable }) => ( @@ -94,6 +97,7 @@ export const DurationEventStartEnd = React.memo<{ data-test-subj="event-end" field={EVENT_END_FIELD_NAME} id={`duration-event-start-end-default-draggable-${contextId}-${eventId}-${EVENT_END_FIELD_NAME}-${end}`} + isDraggable={isDraggable} tooltipContent={null} value={end} > diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx index 4714b561f036be..e319e803e63fe7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx @@ -48,6 +48,7 @@ export const NetflowColumns = React.memo( eventId, eventEnd, eventStart, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -76,6 +77,7 @@ export const NetflowColumns = React.memo( @@ -88,6 +90,7 @@ export const NetflowColumns = React.memo( eventId={eventId} eventEnd={eventEnd} eventStart={eventStart} + isDraggable={isDraggable} /> @@ -104,6 +107,7 @@ export const NetflowColumns = React.memo( destinationPackets={destinationPackets} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} networkBytes={networkBytes} networkCommunityId={networkCommunityId} networkDirection={networkDirection} diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts index 532b35f4cffd00..801df93bfcf372 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts @@ -21,6 +21,7 @@ export interface NetflowColumnsProps { eventId: string; eventEnd?: string[] | null; eventStart?: string[] | null; + isDraggable?: boolean; networkBytes?: string[] | null; networkCommunityId?: string[] | null; networkDirection?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx index e6931baeb70179..72de537fee5889 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx @@ -22,9 +22,10 @@ export const USER_NAME_FIELD_NAME = 'user.name'; export const UserProcess = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; processName?: string[] | null; userName?: string[] | null; -}>(({ contextId, eventId, processName, userName }) => ( +}>(({ contextId, eventId, isDraggable, processName, userName }) => ( @@ -55,6 +57,7 @@ export const UserProcess = React.memo<{ data-test-subj="process-name" eventId={eventId} field={PROCESS_NAME_FIELD_NAME} + isDraggable={isDraggable} value={process} iconType="console" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts b/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts index a28334e2d45fb1..0798345c61da96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts @@ -20,6 +20,7 @@ export interface NetflowProps { eventId: string; eventEnd?: string[] | null; eventStart?: string[] | null; + isDraggable?: boolean; networkBytes?: string[] | null; networkCommunityId?: string[] | null; networkDirection?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx index b0384155c5c10f..d6aa34f2528e53 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx @@ -26,6 +26,7 @@ const AlertsExampleComponent: React.FC = () => { {alertsRowRenderer.renderRow({ browserFields: {}, data: mockEndpointProcessExecutionMalwarePreventionAlert, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 703621bc4c6663..2c6ce5886462b5 100644 --- 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 @@ -23,6 +23,7 @@ const AuditdExampleComponent: React.FC = () => { {auditdRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[26].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 265a71ef264d1a..a525b26571dc8c 100644 --- 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 @@ -23,6 +23,7 @@ const AuditdFileExampleComponent: React.FC = () => { {auditdFileRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[27].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx index 6198225fcb87dd..f8704b63fe47e4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx @@ -23,6 +23,7 @@ const LibraryExampleComponent: React.FC = () => { {libraryRowRenderer.renderRow({ browserFields: {}, data: mockEndpointLibraryLoadEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index cd20b28203246c..c5a0f094408992 100644 --- 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 @@ -16,6 +16,7 @@ const NetflowExampleComponent: React.FC = () => ( {netflowRowRenderer.renderRow({ browserFields: {}, data: getMockNetflowData(), + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx index f00db0d94eed82..67859db1a5ea4d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx @@ -23,6 +23,7 @@ const RegistryExampleComponent: React.FC = () => { {registryRowRenderer.renderRow({ browserFields: {}, data: mockEndpointRegistryModificationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index f22ac0dca6f9d2..1e6caca2effa99 100644 --- 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 @@ -16,6 +16,7 @@ const SuricataExampleComponent: React.FC = () => ( {suricataRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[2].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 909d5224fb3511..7d38f8feaace69 100644 --- 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 @@ -23,6 +23,7 @@ const SystemExampleComponent: React.FC = () => { {systemRowRenderer.renderRow({ browserFields: {}, data: mockEndgameTerminationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 0f413eed811bee..72c5060b277017 100644 --- 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 @@ -19,6 +19,7 @@ const SystemDnsExampleComponent: React.FC = () => { {systemDnsRowRenderer.renderRow({ browserFields: {}, data: mockEndgameDnsRequest, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 0e5fe1768c7871..6103746b3238b1 100644 --- 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 @@ -23,6 +23,7 @@ const SystemEndgameProcessExampleComponent: React.FC = () => { {systemEndgameProcessRowRenderer.renderRow({ browserFields: {}, data: mockEndgameCreationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 3db9a93fc37c94..cb8668536f8d29 100644 --- 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 @@ -23,6 +23,7 @@ const SystemFileExampleComponent: React.FC = () => { {systemFileRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileDeleteEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 08ff6a5ddc7c94..12ad132131d1b3 100644 --- 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 @@ -23,6 +23,7 @@ const SystemFimExampleComponent: React.FC = () => { {systemFimRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileCreateEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 59b5fedbc82fa4..8dfb0bf9987384 100644 --- 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 @@ -21,6 +21,7 @@ const SystemSecurityEventExampleComponent: React.FC = () => { {systemSecurityEventRowRenderer.renderRow({ browserFields: {}, data: mockEndgameUserLogon, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index 5175145bae9d95..7fa430e812625a 100644 --- 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 @@ -22,6 +22,7 @@ const SystemSocketExampleComponent: React.FC = () => { {systemSocketRowRenderer.renderRow({ browserFields: {}, data: mockEndgameIpv4ConnectionAcceptEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx index 9d7e5d48315e37..73d458a23ca17b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx @@ -16,6 +16,7 @@ const ThreatMatchExampleComponent: React.FC = () => ( {threatMatchRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[31].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} 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 index b84942ea8b2a89..83d9e0122e971d 100644 --- 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 @@ -16,6 +16,7 @@ const ZeekExampleComponent: React.FC = () => ( {zeekRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[13].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 19abd6841e7e89..cf1f4a26c709d8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -80,6 +80,7 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, + isDraggable: true, timelineId, })}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap index 92816d499b0296..722c7a7aebb00a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap @@ -19,6 +19,7 @@ exports[`empty_column_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} key="empty-column-renderer-draggable-wrapper-test-source.ip-1-source.ip" render={[Function]} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx index 417cf0ceee1846..edec2d0d823fa8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -42,6 +42,7 @@ export const AgentStatuses = React.memo( @@ -60,6 +61,7 @@ export const AgentStatuses = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx index bdb650585bdb02..fbb2c2edf8ae37 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx @@ -15,9 +15,10 @@ interface Props { contextId: string; eventId: string; processTitle: string | null | undefined; + isDraggable?: boolean; } -export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) => { +export const ArgsComponent = ({ args, contextId, eventId, processTitle, isDraggable }: Props) => { if (isNillEmptyOrNotFinite(args) && isNillEmptyOrNotFinite(processTitle)) { return null; } @@ -31,6 +32,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) contextId={`${contextId}-args-${i}-${arg}`} eventId={eventId} field="process.args" + isDraggable={isDraggable} value={arg} /> @@ -42,6 +44,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) contextId={contextId} eventId={eventId} field="process.title" + isDraggable={isDraggable} value={processTitle} /> 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 63b65d3cf36bed..684764e39848fe 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 @@ -98,6 +98,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga }, } } + isDraggable={true} text="connected using" timelineId="test" /> @@ -222,6 +223,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai } } fileIcon="document" + isDraggable={true} text="opened file using" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx index 737d0b74bfbf91..fb14d44995c95b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx @@ -36,6 +36,7 @@ interface Props { workingDirectory: string | null | undefined; args: string[] | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const AuditdGenericLine = React.memo( @@ -55,6 +56,7 @@ export const AuditdGenericLine = React.memo( result, session, text, + isDraggable, }) => ( ( secondary={secondary} workingDirectory={workingDirectory} session={session} + isDraggable={isDraggable} /> {processExecutable != null && ( @@ -81,9 +84,16 @@ export const AuditdGenericLine = React.memo( processPid={processPid} processName={processName} processExecutable={processExecutable} + isDraggable={isDraggable} /> - + {result != null && ( {i18n.WITH_RESULT} @@ -94,6 +104,7 @@ export const AuditdGenericLine = React.memo( contextId={contextId} eventId={id} field="auditd.result" + isDraggable={isDraggable} queryValue={result} value={result} /> @@ -107,13 +118,14 @@ AuditdGenericLine.displayName = 'AuditdGenericLine'; interface GenericDetailsProps { browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; contextId: string; text: string; timelineId: string; } export const AuditdGenericDetails = React.memo( - ({ data, contextId, text, timelineId }) => { + ({ data, contextId, isDraggable, text, timelineId }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); const hostName: string | null | undefined = get('host.name[0]', data); @@ -146,9 +158,10 @@ export const AuditdGenericDetails = React.memo( primary={primary} result={result} secondary={secondary} + isDraggable={isDraggable} /> - + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx index efab1a433c0bb0..89fbbf751b0eeb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -38,6 +38,7 @@ interface Props { workingDirectory: string | null | undefined; args: string[] | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const AuditdGenericFileLine = React.memo( @@ -59,6 +60,7 @@ export const AuditdGenericFileLine = React.memo( session, text, fileIcon, + isDraggable, }) => ( ( secondary={secondary} workingDirectory={workingDirectory} session={session} + isDraggable={isDraggable} /> {(filePath != null || processExecutable != null) && ( @@ -81,6 +84,7 @@ export const AuditdGenericFileLine = React.memo( contextId={contextId} eventId={id} field="file.path" + isDraggable={isDraggable} value={filePath} iconType={fileIcon} /> @@ -96,12 +100,19 @@ export const AuditdGenericFileLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} /> - + {result != null && ( {i18n.WITH_RESULT} @@ -112,6 +123,7 @@ export const AuditdGenericFileLine = React.memo( contextId={contextId} eventId={id} field="auditd.result" + isDraggable={isDraggable} queryValue={result} value={result} /> @@ -124,15 +136,16 @@ AuditdGenericFileLine.displayName = 'AuditdGenericFileLine'; interface GenericDetailsProps { browserFields: BrowserFields; - data: Ecs; contextId: string; + data: Ecs; text: string; fileIcon: IconType; timelineId: string; + isDraggable?: boolean; } export const AuditdGenericFileDetails = React.memo( - ({ data, contextId, text, fileIcon = 'document', timelineId }) => { + ({ data, contextId, text, fileIcon = 'document', timelineId, isDraggable }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); const hostName: string | null | undefined = get('host.name[0]', data); @@ -169,9 +182,10 @@ export const AuditdGenericFileDetails = React.memo( secondary={secondary} fileIcon={fileIcon} result={result} + isDraggable={isDraggable} /> - + ); } else { 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 74a5ff472b5815..1f44feb3b394f6 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 @@ -55,6 +55,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); @@ -84,6 +85,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -117,6 +119,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: auditdFile, + isDraggable: true, timelineId: 'test', }); @@ -146,6 +149,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditdFile, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( 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 765bfd3d21351b..d0522e97157abf 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 @@ -36,11 +36,12 @@ export const createGenericAuditRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx index 8fd8cfd5af9da6..5857dc1e301824 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx @@ -21,69 +21,77 @@ interface Props { eventId: string; primary: string | null | undefined; secondary: string | null | undefined; + isDraggable?: boolean; } -export const PrimarySecondary = React.memo(({ contextId, eventId, primary, secondary }) => { - if (nilOrUnSet(primary) && nilOrUnSet(secondary)) { - return null; - } else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) { - return ( - - ); - } else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) { - return ( - - ); - } else if (primary === secondary) { - return ( - - ); - } else { - return ( - - - - - - {i18n.AS} - - - - - - ); +export const PrimarySecondary = React.memo( + ({ contextId, eventId, primary, secondary, isDraggable }) => { + if (nilOrUnSet(primary) && nilOrUnSet(secondary)) { + return null; + } else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) { + return ( + + ); + } else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) { + return ( + + ); + } else if (primary === secondary) { + return ( + + ); + } else { + return ( + + + + + + {i18n.AS} + + + + + + ); + } } -}); +); PrimarySecondary.displayName = 'PrimarySecondary'; @@ -93,10 +101,11 @@ interface PrimarySecondaryUserInfoProps { userName: string | null | undefined; primary: string | null | undefined; secondary: string | null | undefined; + isDraggable?: boolean; } export const PrimarySecondaryUserInfo = React.memo( - ({ contextId, eventId, userName, primary, secondary }) => { + ({ contextId, eventId, userName, primary, secondary, isDraggable }) => { if (nilOrUnSet(userName) && nilOrUnSet(primary) && nilOrUnSet(secondary)) { return null; } else if ( @@ -111,6 +120,7 @@ export const PrimarySecondaryUserInfo = React.memo @@ -121,6 +131,7 @@ export const PrimarySecondaryUserInfo = React.memo @@ -130,6 +141,7 @@ export const PrimarySecondaryUserInfo = React.memo diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx index a7252064d9774e..f90407b882fdf5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx @@ -23,10 +23,21 @@ interface Props { secondary: string | null | undefined; workingDirectory: string | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const SessionUserHostWorkingDir = React.memo( - ({ eventId, contextId, hostName, userName, primary, secondary, workingDirectory, session }) => ( + ({ + eventId, + contextId, + hostName, + userName, + primary, + secondary, + workingDirectory, + session, + isDraggable, + }) => ( <> {i18n.SESSION} @@ -38,6 +49,7 @@ export const SessionUserHostWorkingDir = React.memo( field="auditd.session" value={session} iconType="number" + isDraggable={isDraggable} /> @@ -47,6 +59,7 @@ export const SessionUserHostWorkingDir = React.memo( userName={userName} primary={primary} secondary={secondary} + isDraggable={isDraggable} /> {hostName != null && ( @@ -59,6 +72,7 @@ export const SessionUserHostWorkingDir = React.memo( eventId={eventId} workingDirectory={workingDirectory} hostName={hostName} + isDraggable={isDraggable} /> ) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx index e2418334dfc804..8859c601ad56df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx @@ -26,6 +26,7 @@ export const Bytes = React.memo<{ isDraggable ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx index 11846632f740ea..f2e4555147c500 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -26,6 +26,7 @@ interface IndicatorDetailsProps { indicatorProvider: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; + isDraggable?: boolean; } export const IndicatorDetails: React.FC = ({ @@ -35,6 +36,7 @@ export const IndicatorDetails: React.FC = ({ indicatorProvider, indicatorReference, indicatorType, + isDraggable, }) => ( = ({ data-test-subj="threat-match-indicator-details-indicator-type" eventId={eventId} field={INDICATOR_MATCHED_TYPE} + isDraggable={isDraggable} value={indicatorType} /> @@ -71,6 +74,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-dataset" eventId={eventId} field={INDICATOR_DATASET} + isDraggable={isDraggable} value={indicatorDataset} /> @@ -92,6 +96,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-provider" eventId={eventId} field={INDICATOR_PROVIDER} + isDraggable={isDraggable} value={indicatorProvider} /> @@ -108,6 +113,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-reference" eventId={eventId} fieldName={INDICATOR_REFERENCE} + isDraggable={isDraggable} value={indicatorReference} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx index 2195421301d318..31c5065cde59a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx @@ -16,6 +16,7 @@ import { HorizontalSpacer } from './helpers'; interface MatchDetailsProps { contextId: string; eventId: string; + isDraggable?: boolean; sourceField: string; sourceValue: string; } @@ -23,6 +24,7 @@ interface MatchDetailsProps { export const MatchDetails: React.FC = ({ contextId, eventId, + isDraggable, sourceField, sourceValue, }) => ( @@ -40,6 +42,7 @@ export const MatchDetails: React.FC = ({ data-test-subj="threat-match-details-source-field" eventId={eventId} field={INDICATOR_MATCHED_FIELD} + isDraggable={isDraggable} value={sourceField} /> @@ -57,6 +60,7 @@ export const MatchDetails: React.FC = ({ data-test-subj="threat-match-details-source-value" eventId={eventId} field={sourceField} + isDraggable={isDraggable} value={sourceValue} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index ba5b0127df526b..94ed19e218d746 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -28,6 +28,7 @@ export interface ThreatMatchRowProps { indicatorProvider: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; + isDraggable?: boolean; sourceField: string; sourceValue: string; } @@ -36,10 +37,12 @@ export const ThreatMatchRow = ({ contextId, data, eventId, + isDraggable, }: { contextId: string; data: Fields; eventId: string; + isDraggable?: boolean; }) => { const props = { contextId, @@ -48,6 +51,7 @@ export const ThreatMatchRow = ({ indicatorReference: get(data, EVENT_REFERENCE)[0] as string | undefined, indicatorProvider: get(data, PROVIDER)[0] as string | undefined, indicatorType: get(data, MATCHED_TYPE)[0] as string | undefined, + isDraggable, sourceField: get(data, MATCHED_FIELD)[0] as string, sourceValue: get(data, MATCHED_ATOMIC)[0] as string, }; @@ -62,6 +66,7 @@ export const ThreatMatchRowView = ({ indicatorProvider, indicatorReference, indicatorType, + isDraggable, sourceField, sourceValue, }: ThreatMatchRowProps) => { @@ -76,6 +81,7 @@ export const ThreatMatchRowView = ({ @@ -88,6 +94,7 @@ export const ThreatMatchRowView = ({ indicatorProvider={indicatorProvider} indicatorReference={indicatorReference} indicatorType={indicatorType} + isDraggable={isDraggable} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx index 6687179e5b8878..78972442f5018d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx @@ -56,6 +56,7 @@ describe('threatMatchRowRenderer', () => { const children = threatMatchRowRenderer.renderRow({ browserFields: {}, data: threatMatchData, + isDraggable: true, timelineId: 'test', }); const wrapper = shallow({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index f6feb6dd1b126a..d2c1f09d903c10 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -20,7 +20,7 @@ const SpacedContainer = styled.div` margin: ${({ theme }) => theme.eui.paddingSizes.s} 0; `; -export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) => { +export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, isDraggable, timelineId }) => { const indicators = get(data, 'threat.indicator') as Fields[]; const eventId = get(data, ID_FIELD_NAME); @@ -31,7 +31,12 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) const contextId = `threat-match-row-${timelineId}-${eventId}-${index}`; return ( - + {index < indicators.length - 1 && } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx index 3a53db2196d8c1..90d68eceb7fb3d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx @@ -20,46 +20,50 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const DnsRequestEventDetails = React.memo(({ data, contextId, timelineId }) => { - const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data); - const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data); - const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data); - const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data); - const eventCode: string | null | undefined = get('event.code[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const id = data._id; - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); +export const DnsRequestEventDetails = React.memo( + ({ data, contextId, isDraggable, timelineId }) => { + const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data); + const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data); + const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data); + const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); - return ( -
- - - -
- ); -}); + return ( +
+ + + +
+ ); + } +); DnsRequestEventDetails.displayName = 'DnsRequestEventDetails'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx index 549abcf6a6d358..ff85336bd47f88 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx @@ -24,6 +24,7 @@ interface Props { eventCode: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processExecutable: string | null | undefined; processName: string | null | undefined; processPid: number | null | undefined; @@ -42,6 +43,7 @@ export const DnsRequestEventDetailsLine = React.memo( eventCode, hostName, id, + isDraggable, processExecutable, processName, processPid, @@ -56,6 +58,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} hostName={hostName} + isDraggable={isDraggable} userDomain={userDomain} userName={userName} workingDirectory={undefined} @@ -71,6 +74,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.question.name" + isDraggable={isDraggable} value={dnsQuestionName} />
@@ -87,6 +91,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.question.type" + isDraggable={isDraggable} value={dnsQuestionType} />
@@ -103,6 +108,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.resolved_ip" + isDraggable={isDraggable} value={dnsResolvedIp} /> @@ -122,6 +128,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.response_code" + isDraggable={isDraggable} value={dnsResponseCode} /> @@ -141,6 +148,7 @@ export const DnsRequestEventDetailsLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -155,6 +163,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="event.code" + isDraggable={isDraggable} value={eventCode} /> @@ -165,6 +174,7 @@ export const DnsRequestEventDetailsLine = React.memo( eventId={id} iconType="logoWindows" field="winlog.event_id" + isDraggable={isDraggable} value={winlogEventId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx index 8e2335a2f149b9..db568726f1b208 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx @@ -60,6 +60,7 @@ export const emptyColumnRenderer: ColumnRenderer = { kqlQuery: '', and: [], }} + isDraggable={isDraggable} key={`empty-column-renderer-draggable-wrapper-${timelineId}-${columnName}-${eventId}-${field.id}`} render={(dataProvider, _, snapshot) => snapshot.isDragging ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx index 515db45e9fcd48..8f39cf933570f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -20,65 +20,75 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const EndgameSecurityEventDetails = React.memo(({ data, contextId, timelineId }) => { - const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data); - const endgameSubjectDomainName: string | null | undefined = get( - 'endgame.subject_domain_name[0]', - data - ); - const endgameSubjectLogonId: string | null | undefined = get('endgame.subject_logon_id[0]', data); - const endgameSubjectUserName: string | null | undefined = get( - 'endgame.subject_user_name[0]', - data - ); - const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data); - const endgameTargetDomainName: string | null | undefined = get( - 'endgame.target_domain_name[0]', - data - ); - const endgameTargetUserName: string | null | undefined = get('endgame.target_user_name[0]', data); - const eventAction: string | null | undefined = get('event.action[0]', data); - const eventCode: string | null | undefined = get('event.code[0]', data); - const eventOutcome: string | null | undefined = get('event.outcome[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const id = data._id; - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); +export const EndgameSecurityEventDetails = React.memo( + ({ data, contextId, isDraggable, timelineId }) => { + const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data); + const endgameSubjectDomainName: string | null | undefined = get( + 'endgame.subject_domain_name[0]', + data + ); + const endgameSubjectLogonId: string | null | undefined = get( + 'endgame.subject_logon_id[0]', + data + ); + const endgameSubjectUserName: string | null | undefined = get( + 'endgame.subject_user_name[0]', + data + ); + const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data); + const endgameTargetDomainName: string | null | undefined = get( + 'endgame.target_domain_name[0]', + data + ); + const endgameTargetUserName: string | null | undefined = get( + 'endgame.target_user_name[0]', + data + ); + const eventAction: string | null | undefined = get('event.action[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const eventOutcome: string | null | undefined = get('event.outcome[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); - return ( -
- - - -
- ); -}); + return ( +
+ + + +
+ ); + } +); EndgameSecurityEventDetails.displayName = 'EndgameSecurityEventDetails'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx index aba6f7346271d7..7e5a6dd08765bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -38,6 +38,7 @@ interface Props { eventOutcome: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processExecutable: string | null | undefined; processName: string | null | undefined; processPid: number | null | undefined; @@ -61,6 +62,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( eventOutcome, hostName, id, + isDraggable, processExecutable, processName, processPid, @@ -95,6 +97,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( eventId={id} hostName={hostName} hostNameSeparator={hostNameSeparator} + isDraggable={isDraggable} userDomain={domain} userDomainField={userDomainField} userName={user} @@ -116,6 +119,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.logon_type" + isDraggable={isDraggable} queryValue={String(endgameLogonType)} value={`${endgameLogonType} - ${getHumanReadableLogonType(endgameLogonType)}`} /> @@ -136,6 +140,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.target_logon_id" + isDraggable={isDraggable} value={endgameTargetLogonId} /> @@ -155,6 +160,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -176,6 +182,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_user_name" + isDraggable={isDraggable} iconType="user" value={endgameSubjectUserName} /> @@ -197,6 +204,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_domain_name" + isDraggable={isDraggable} value={endgameSubjectDomainName} /> @@ -216,6 +224,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_logon_id" + isDraggable={isDraggable} value={endgameSubjectLogonId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx index 7ac9fe290893f8..1f1862daa4e55d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx @@ -15,12 +15,13 @@ interface Props { contextId: string; endgameExitCode: string | null | undefined; eventId: string; + isDraggable?: boolean; processExitCode: number | null | undefined; text: string | null | undefined; } export const ExitCodeDraggable = React.memo( - ({ contextId, endgameExitCode, eventId, processExitCode, text }) => { + ({ contextId, endgameExitCode, eventId, isDraggable, processExitCode, text }) => { if (isNillEmptyOrNotFinite(processExitCode) && isNillEmptyOrNotFinite(endgameExitCode)) { return null; } @@ -39,6 +40,7 @@ export const ExitCodeDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.exit_code" + isDraggable={isDraggable} value={`${processExitCode}`} /> @@ -50,6 +52,7 @@ export const ExitCodeDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.exit_code" + isDraggable={isDraggable} value={endgameExitCode} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx index 703b38e627e550..7ff5a0f73ab302 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx @@ -20,6 +20,7 @@ interface Props { fileName: string | null | undefined; filePath: string | null | undefined; fileExtOriginalPath: string | null | undefined; + isDraggable?: boolean; } export const FileDraggable = React.memo( @@ -31,6 +32,7 @@ export const FileDraggable = React.memo( fileExtOriginalPath, fileName, filePath, + isDraggable, }) => { if ( isNillEmptyOrNotFinite(fileName) && @@ -52,6 +54,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.name" + isDraggable={isDraggable} value={fileName} iconType="document" /> @@ -62,6 +65,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.file_name" + isDraggable={isDraggable} value={endgameFileName} iconType="document" /> @@ -80,6 +84,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.path" + isDraggable={isDraggable} value={filePath} iconType="document" /> @@ -90,6 +95,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.file_path" + isDraggable={isDraggable} value={endgameFilePath} iconType="document" /> @@ -106,6 +112,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.Ext.original.path" + isDraggable={isDraggable} value={fileExtOriginalPath} iconType="document" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx index 9e624ba17c9212..13b024e0a5359a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx @@ -21,9 +21,10 @@ interface Props { contextId: string; eventId: string; fileHashSha256: string | null | undefined; + isDraggable?: boolean; } -export const FileHash = React.memo(({ contextId, eventId, fileHashSha256 }) => { +export const FileHash = React.memo(({ contextId, eventId, fileHashSha256, isDraggable }) => { if (isNillEmptyOrNotFinite(fileHashSha256)) { return null; } @@ -35,6 +36,7 @@ export const FileHash = React.memo(({ contextId, eventId, fileHashSha256 contextId={contextId} eventId={eventId} field="file.hash.sha256" + isDraggable={isDraggable} iconType="number" value={fileHashSha256} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index aa6c7beb9139e3..06ed9011109625 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -86,6 +86,7 @@ const FormattedFieldValueComponent: React.FC<{ @@ -214,6 +215,7 @@ const FormattedFieldValueComponent: React.FC<{ = ({ @@ -95,6 +96,7 @@ export const RenderRuleName: React.FC = ({ @@ -150,6 +152,7 @@ export const renderEventModule = ({ @@ -218,6 +221,7 @@ export const renderUrl = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 104550f138f16e..6b76aba92678da 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -54,6 +54,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); @@ -66,6 +67,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -81,6 +83,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -99,6 +102,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -117,6 +121,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -135,6 +140,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -153,6 +159,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index abd4731ec4b668..060b539950d837 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -93,6 +93,7 @@ const HostNameComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx index de307d1af7f93f..fef9a5d5c02014 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx @@ -17,10 +17,11 @@ interface Props { eventId: string; hostName: string | null | undefined; workingDirectory: string | null | undefined; + isDraggable?: boolean; } export const HostWorkingDir = React.memo( - ({ contextId, eventId, hostName, workingDirectory }) => ( + ({ contextId, eventId, hostName, workingDirectory, isDraggable }) => ( <> ( eventId={eventId} field="host.name" value={hostName} + isDraggable={isDraggable} /> {workingDirectory != null && ( @@ -42,6 +44,7 @@ export const HostWorkingDir = React.memo( field="process.working_directory" value={workingDirectory} iconType="folderOpen" + isDraggable={isDraggable} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx index 18f56d8b030669..d6ea939c966ac7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx @@ -60,52 +60,58 @@ import { interface NetflowRendererProps { data: Ecs; timelineId: string; + isDraggable?: boolean; } -export const NetflowRenderer = React.memo(({ data, timelineId }) => ( - -)); +export const NetflowRenderer = React.memo( + ({ data, timelineId, isDraggable }) => ( + + ) +); NetflowRenderer.displayName = 'NetflowRenderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap index d7bdacbcc61efa..a9ecbe8428aeea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap @@ -67,6 +67,7 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = ` "2018-11-12T19:03:25.836Z", ] } + isDraggable={true} networkBytes={ Array [ 100, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index fc97624dbfc961..01e05bbc365e90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -38,6 +38,7 @@ describe('netflowRowRenderer', () => { const children = netflowRowRenderer.renderRow({ browserFields, data: getMockNetflowData(), + isDraggable: true, timelineId: 'test', }); @@ -107,6 +108,7 @@ describe('netflowRowRenderer', () => { const children = netflowRowRenderer.renderRow({ browserFields: mockBrowserFields, data: getMockNetflowData(), + isDraggable: true, timelineId: 'test', }); const wrapper = mount( 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 35406dce6ff72b..272912b855af00 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 @@ -90,7 +90,7 @@ export const netflowRowRenderer: RowRenderer = { isInstance: (ecs) => eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), - renderRow: ({ data, timelineId }) => ( + renderRow: ({ data, isDraggable, timelineId }) => (
( contextId, endgameParentProcessName, eventId, + isDraggable, processParentName, processParentPid, processPpid, @@ -56,6 +58,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.parent.name" + isDraggable={isDraggable} value={processParentName} /> @@ -67,6 +70,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.parent_process_name" + isDraggable={isDraggable} value={endgameParentProcessName} /> @@ -78,6 +82,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.parent.pid" + isDraggable={isDraggable} queryValue={String(processParentPid)} value={`(${String(processParentPid)})`} /> @@ -90,6 +95,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.ppid" + isDraggable={isDraggable} queryValue={String(processPpid)} value={`(${String(processPpid)})`} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx index 666fb254aaa2cd..f28c72253a4e79 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -24,6 +24,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: true, timelineId: 'test', }); const wrapper = shallow({children}); @@ -38,6 +39,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: true, timelineId: 'test', }); const wrapper = mount({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx index 705eff88732047..db7e3ae6f06c93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx @@ -21,6 +21,7 @@ interface Props { processExecutable: string | undefined | null; processPid: number | undefined | null; processName: string | undefined | null; + isDraggable?: boolean; } export const ProcessDraggable = React.memo( @@ -32,6 +33,7 @@ export const ProcessDraggable = React.memo( processExecutable, processName, processPid, + isDraggable, }) => { if ( isNillEmptyOrNotFinite(processName) && @@ -53,6 +55,7 @@ export const ProcessDraggable = React.memo( field="process.name" value={processName} iconType="console" + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(processExecutable) ? ( @@ -63,6 +66,7 @@ export const ProcessDraggable = React.memo( field="process.executable" value={processExecutable} iconType="console" + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(endgameProcessName) ? ( @@ -73,6 +77,7 @@ export const ProcessDraggable = React.memo( field="endgame.process_name" value={endgameProcessName} iconType="console" + isDraggable={isDraggable} /> ) : null} @@ -85,6 +90,7 @@ export const ProcessDraggable = React.memo( field="process.pid" queryValue={String(processPid)} value={`(${String(processPid)})`} + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(endgamePid) ? ( @@ -95,6 +101,7 @@ export const ProcessDraggable = React.memo( field="endgame.pid" queryValue={String(endgamePid)} value={`(${String(endgamePid)})`} + isDraggable={isDraggable} /> ) : null} @@ -114,6 +121,7 @@ export const ProcessDraggableWithNonExistentProcess = React.memo( processExecutable, processName, processPid, + isDraggable, }) => { if ( endgamePid == null && @@ -133,6 +141,7 @@ export const ProcessDraggableWithNonExistentProcess = React.memo( processExecutable={processExecutable} processName={processName} processPid={processPid} + isDraggable={isDraggable} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx index 32432afbf205c0..dd4f588a14bb71 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx @@ -20,27 +20,31 @@ const HashFlexGroup = styled(EuiFlexGroup)` interface Props { contextId: string; eventId: string; + isDraggable?: boolean; processHashSha256: string | null | undefined; } -export const ProcessHash = React.memo(({ contextId, eventId, processHashSha256 }) => { - if (isNillEmptyOrNotFinite(processHashSha256)) { - return null; +export const ProcessHash = React.memo( + ({ contextId, eventId, isDraggable, processHashSha256 }) => { + if (isNillEmptyOrNotFinite(processHashSha256)) { + return null; + } + + return ( + + + + + + ); } - - return ( - - - - - - ); -}); +); ProcessHash.displayName = 'ProcessHash'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx index 0bfb03168019a5..da31f75e2fa10b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx @@ -18,10 +18,11 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; text: string; } -const RegistryEventDetailsComponent: React.FC = ({ contextId, data, text }) => { +const RegistryEventDetailsComponent: React.FC = ({ contextId, data, isDraggable, text }) => { const hostName: string | null | undefined = get('host.name[0]', data); const id = data._id; const processName: string | null | undefined = get('process.name[0]', data); @@ -41,6 +42,7 @@ const RegistryEventDetailsComponent: React.FC = ({ contextId, data, text contextId={contextId} hostName={hostName} id={id} + isDraggable={isDraggable} processName={processName} processPid={processPid} registryKey={registryKey} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx index b85ae25ed25093..8d9f52da88fdde 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx @@ -19,6 +19,7 @@ interface Props { contextId: string; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processName: string | null | undefined; processPid: number | null | undefined; registryKey: string | null | undefined; @@ -32,6 +33,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId, hostName, id, + isDraggable, processName, processPid, registryKey, @@ -71,6 +73,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} hostName={hostName} + isDraggable={isDraggable} userDomain={userDomain} userName={userName} workingDirectory={undefined} @@ -86,6 +89,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} field="registry.key" + isDraggable={isDraggable} tooltipContent={registryKeyTooltipContent} value={registryKey} /> @@ -103,6 +107,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} field="registry.path" + isDraggable={isDraggable} tooltipContent={registryPathTooltipContent} value={registryPath} /> @@ -120,6 +125,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={undefined} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index 126bfae996ef7f..09248b832490a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -42,6 +42,7 @@ const RuleStatusComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 2934d35dc184da..eeb8786b2cfa3d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -525,6 +525,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx index 82eb11d455543b..f096cd906f619c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx @@ -26,8 +26,9 @@ Details.displayName = 'Details'; export const SuricataDetails = React.memo<{ browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; timelineId: string; -}>(({ data, timelineId }) => { +}>(({ data, isDraggable, timelineId }) => { const signature: string | null | undefined = get('suricata.eve.alert.signature[0]', data); const signatureId: number | null | undefined = get('suricata.eve.alert.signature_id[0]', data); @@ -37,12 +38,13 @@ export const SuricataDetails = React.memo<{ - +
); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 998233b2278c9e..661fc562cc34c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -45,6 +45,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); @@ -64,6 +65,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -81,6 +83,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( 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 aa482926bf007e..0faa6a4fbba746 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 @@ -21,9 +21,14 @@ export const suricataRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( - + ), }; 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 a4e16c66f4fef6..2a5b57d77498f0 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 @@ -57,65 +57,69 @@ export const Tokens = React.memo<{ tokens: string[] }>(({ tokens }) => ( Tokens.displayName = 'Tokens'; -export const DraggableSignatureId = React.memo<{ id: string; signatureId: number }>( - ({ id, signatureId }) => { - const dataProviderProp = useMemo( - () => ({ - and: [], - enabled: true, - id: escapeDataProviderId(`suricata-draggable-signature-id-${id}-sig-${signatureId}`), - name: String(signatureId), - excluded: false, - kqlQuery: '', - queryMatch: { - field: SURICATA_SIGNATURE_ID_FIELD_NAME, - value: signatureId, - operator: IS_OPERATOR as QueryOperator, - }, - }), - [id, signatureId] - ); - - const render = useCallback( - (dataProvider, _, snapshot) => - snapshot.isDragging ? ( - - - - ) : ( - - - {signatureId} - - - ), - [signatureId] - ); - - return ( - - - - ); - } -); +export const DraggableSignatureId = React.memo<{ + id: string; + isDraggable?: boolean; + signatureId: number; +}>(({ id, isDraggable, signatureId }) => { + const dataProviderProp = useMemo( + () => ({ + and: [], + enabled: true, + id: escapeDataProviderId(`suricata-draggable-signature-id-${id}-sig-${signatureId}`), + name: String(signatureId), + excluded: false, + kqlQuery: '', + queryMatch: { + field: SURICATA_SIGNATURE_ID_FIELD_NAME, + value: signatureId, + operator: IS_OPERATOR as QueryOperator, + }, + }), + [id, signatureId] + ); + + const render = useCallback( + (dataProvider, _, snapshot) => + snapshot.isDragging ? ( + + + + ) : ( + + + {signatureId} + + + ), + [signatureId] + ); + + return ( + + + + ); +}); DraggableSignatureId.displayName = 'DraggableSignatureId'; export const SuricataSignature = React.memo<{ contextId: string; id: string; + isDraggable?: boolean; signature: string; signatureId: number; -}>(({ contextId, id, signature, signatureId }) => { +}>(({ contextId, id, isDraggable, signature, signatureId }) => { const tokens = getBeginningTokens(signature); return ( @@ -124,6 +128,7 @@ export const SuricataSignature = React.memo<{ data-test-subj="draggable-signature-link" field={SURICATA_SIGNATURE_FIELD_NAME} id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} + isDraggable={isDraggable} value={signature} >
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap index d2c405a46acf8f..15443058f434ed 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap @@ -56,6 +56,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai }, } } + isDraggable={true} text="some text" timelineId="test" /> @@ -119,6 +120,7 @@ exports[`GenericRowRenderer #createGenericSystemRowRenderer renders correctly ag }, } } + isDraggable={true} text="some text" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx index 4dcb90637a8179..7de03a2ae23565 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx @@ -13,35 +13,40 @@ import { TokensFlexItem } from '../helpers'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; sshSignature: string | null | undefined; sshMethod: string | null | undefined; } -export const AuthSsh = React.memo(({ contextId, eventId, sshSignature, sshMethod }) => ( - <> - {sshSignature != null && ( - - - - )} - {sshMethod != null && ( - - - - )} - -)); +export const AuthSsh = React.memo( + ({ contextId, eventId, isDraggable, sshSignature, sshMethod }) => ( + <> + {sshSignature != null && ( + + + + )} + {sshMethod != null && ( + + + + )} + + ) +); AuthSsh.displayName = 'AuthSsh'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx index 19d5fd2a0dab12..2cf42ecc9c6707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx @@ -27,6 +27,7 @@ interface Props { contextId: string; hostName: string | null | undefined; id: string; + isDraggable?: boolean; message: string | null | undefined; outcome: string | null | undefined; packageName: string | null | undefined; @@ -48,6 +49,7 @@ export const SystemGenericLine = React.memo( contextId, hostName, id, + isDraggable, message, outcome, packageName, @@ -68,9 +70,10 @@ export const SystemGenericLine = React.memo( @@ -82,6 +85,7 @@ export const SystemGenericLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -97,6 +101,7 @@ export const SystemGenericLine = React.memo( contextId={contextId} eventId={id} field="event.outcome" + isDraggable={isDraggable} queryValue={outcome} value={outcome} /> @@ -104,12 +109,14 @@ export const SystemGenericLine = React.memo( ( - ({ data, contextId, text, timelineId }) => { + ({ contextId, data, isDraggable, text, timelineId }) => { const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; const hostName: string | null | undefined = get('host.name[0]', data); @@ -165,6 +173,7 @@ export const SystemGenericDetails = React.memo( contextId={contextId} hostName={hostName} id={id} + isDraggable={isDraggable} message={message} outcome={outcome} packageName={packageName} @@ -181,7 +190,7 @@ export const SystemGenericDetails = React.memo( workingDirectory={workingDirectory} /> - + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx index 6df583656ff2de..ae31dbff7f0634 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx @@ -45,6 +45,7 @@ interface Props { filePath: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; message: string | null | undefined; outcome: string | null | undefined; packageName: string | null | undefined; @@ -87,6 +88,7 @@ export const SystemGenericFileLine = React.memo( filePath, hostName, id, + isDraggable, message, outcome, packageName, @@ -116,6 +118,7 @@ export const SystemGenericFileLine = React.memo( ( fileExtOriginalPath={fileExtOriginalPath} fileName={fileName} filePath={filePath} + isDraggable={isDraggable} /> )} {showVia(eventAction) && ( @@ -147,6 +151,7 @@ export const SystemGenericFileLine = React.memo( endgamePid={endgamePid} endgameProcessName={endgameProcessName} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -157,6 +162,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameExitCode={endgameExitCode} eventId={id} + isDraggable={isDraggable} processExitCode={processExitCode} text={i18n.WITH_EXIT_CODE} /> @@ -165,6 +171,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameParentProcessName={endgameParentProcessName} eventId={id} + isDraggable={isDraggable} processParentName={processParentName} processParentPid={processParentPid} processPpid={processPpid} @@ -181,6 +188,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} eventId={id} field="event.outcome" + isDraggable={isDraggable} queryValue={outcome} value={outcome} /> @@ -188,22 +196,34 @@ export const SystemGenericFileLine = React.memo( {!skipRedundantFileDetails && ( - + )} {!skipRedundantProcessDetails && ( - + )} {message != null && showMessage && ( @@ -226,8 +246,9 @@ SystemGenericFileLine.displayName = 'SystemGenericFileLine'; interface GenericDetailsProps { browserFields: BrowserFields; - data: Ecs; contextId: string; + data: Ecs; + isDraggable?: boolean; showMessage?: boolean; skipRedundantFileDetails?: boolean; skipRedundantProcessDetails?: boolean; @@ -237,8 +258,9 @@ interface GenericDetailsProps { export const SystemGenericFileDetails = React.memo( ({ - data, contextId, + data, + isDraggable, showMessage = true, skipRedundantFileDetails = false, skipRedundantProcessDetails = false, @@ -323,9 +345,10 @@ export const SystemGenericFileDetails = React.memo( sshSignature={sshSignature} sshMethod={sshMethod} outcome={outcome} + isDraggable={isDraggable} /> - + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 6f5b225f0690bf..516d2797659043 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -118,6 +118,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields, data: system, + isDraggable: true, timelineId: 'test', }); @@ -147,6 +148,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: system, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -180,6 +182,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: systemFile, + isDraggable: true, timelineId: 'test', }); @@ -208,6 +211,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: systemFile, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -239,6 +243,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -266,6 +271,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -295,6 +301,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFilesEncryptedRansomwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -324,6 +331,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFilesEncryptedRansomwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -353,6 +361,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -382,6 +391,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -409,6 +419,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -436,6 +447,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -465,6 +477,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecutionMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -494,6 +507,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecutionMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -521,6 +535,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecEvent, + isDraggable: true, timelineId: 'test', })} @@ -546,6 +561,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessForkEvent, + isDraggable: true, timelineId: 'test', })} @@ -571,6 +587,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessStartEvent, + isDraggable: true, timelineId: 'test', })} @@ -599,6 +616,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -624,6 +642,7 @@ describe('GenericRowRenderer', () => { endpointProcessEndRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessEndEvent, + isDraggable: true, timelineId: 'test', })} @@ -652,6 +671,7 @@ describe('GenericRowRenderer', () => { endgameProcessTerminationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameTerminationEvent, + isDraggable: true, timelineId: 'test', })} @@ -680,6 +700,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -710,6 +731,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -740,6 +762,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -765,6 +788,7 @@ describe('GenericRowRenderer', () => { endpointFileCreationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -793,6 +817,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -818,6 +843,7 @@ describe('GenericRowRenderer', () => { endpointFileDeletionRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileDeletionEvent, + isDraggable: true, timelineId: 'test', })} @@ -843,6 +869,7 @@ describe('GenericRowRenderer', () => { endpointFileModificationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationEvent, + isDraggable: true, timelineId: 'test', })} @@ -868,6 +895,7 @@ describe('GenericRowRenderer', () => { endpointFileOverwriteRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileOverwriteEvent, + isDraggable: true, timelineId: 'test', })} @@ -893,6 +921,7 @@ describe('GenericRowRenderer', () => { endpointFileRenameRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameEvent, + isDraggable: true, timelineId: 'test', })} @@ -921,6 +950,7 @@ describe('GenericRowRenderer', () => { endgameFileDeleteEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileDeleteEvent, + isDraggable: true, timelineId: 'test', })} @@ -949,6 +979,7 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, + isDraggable: true, timelineId: 'test', })} @@ -975,6 +1006,7 @@ describe('GenericRowRenderer', () => { fileDeletedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileDeletedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1003,6 +1035,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -1033,6 +1066,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -1063,6 +1097,7 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1090,6 +1125,7 @@ describe('GenericRowRenderer', () => { endpointConnectionAcceptedRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkConnectionAcceptedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1118,6 +1154,7 @@ describe('GenericRowRenderer', () => { endpointRegistryModificationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointRegistryModificationEvent, + isDraggable: true, timelineId: 'test', })} @@ -1145,6 +1182,7 @@ describe('GenericRowRenderer', () => { endpointLibraryLoadRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointLibraryLoadEvent, + isDraggable: true, timelineId: 'test', })} @@ -1171,6 +1209,7 @@ describe('GenericRowRenderer', () => { endpointHttpRequestEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkHttpRequestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1199,6 +1238,7 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1227,6 +1267,7 @@ describe('GenericRowRenderer', () => { endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1252,6 +1293,7 @@ describe('GenericRowRenderer', () => { endpointDisconnectReceivedRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointDisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1280,6 +1322,7 @@ describe('GenericRowRenderer', () => { endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4DisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1308,6 +1351,7 @@ describe('GenericRowRenderer', () => { endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6DisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1336,6 +1380,7 @@ describe('GenericRowRenderer', () => { socketOpenedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketOpenedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1364,6 +1409,7 @@ describe('GenericRowRenderer', () => { socketClosedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketClosedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1392,6 +1438,7 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1413,6 +1460,7 @@ describe('GenericRowRenderer', () => { securityLogOnRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOnSuccessEvent, + isDraggable: true, timelineId: 'test', })} @@ -1434,6 +1482,7 @@ describe('GenericRowRenderer', () => { securityLogOnRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOnFailureEvent, + isDraggable: true, timelineId: 'test', })} @@ -1458,6 +1507,7 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1482,6 +1532,7 @@ describe('GenericRowRenderer', () => { adminLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: adminLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1506,6 +1557,7 @@ describe('GenericRowRenderer', () => { explicitUserLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: explicitUserLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1527,6 +1579,7 @@ describe('GenericRowRenderer', () => { securityLogOffRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOffEvent, + isDraggable: true, timelineId: 'test', })} @@ -1551,6 +1604,7 @@ describe('GenericRowRenderer', () => { userLogoffEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogoffEvent, + isDraggable: true, timelineId: 'test', })} @@ -1575,6 +1629,7 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1594,6 +1649,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkLookupRequestedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1613,6 +1669,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkLookupResultEvent, + isDraggable: true, timelineId: 'test', })} @@ -1636,6 +1693,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1659,6 +1717,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: dnsEvent, + isDraggable: true, timelineId: 'test', })} @@ -1688,6 +1747,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1715,6 +1775,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} 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 c6845d7d672d26..b1027bf12b7d2a 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 @@ -40,12 +40,13 @@ export const createGenericSystemRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -71,12 +72,13 @@ export const createEndgameProcessRowRenderer = ({ action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -239,12 +245,13 @@ export const createSocketRowRenderer = ({ const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -268,12 +275,13 @@ export const createSecurityEventRowRenderer = ({ action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -287,12 +295,13 @@ export const createDnsRowRenderer = (): RowRenderer => ({ const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -315,12 +324,13 @@ export const createEndpointRegistryRowRenderer = ({ dataset?.toLowerCase() === 'endpoint.events.registry' && action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx index 7952154da12939..296c099da22a44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx @@ -13,13 +13,14 @@ import { TokensFlexItem } from '../helpers'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; packageName: string | null | undefined; packageSummary: string | null | undefined; packageVersion: string | null | undefined; } export const Package = React.memo( - ({ contextId, eventId, packageName, packageSummary, packageVersion }) => { + ({ contextId, eventId, isDraggable, packageName, packageSummary, packageVersion }) => { if (packageName != null || packageSummary != null || packageVersion != null) { return ( <> @@ -28,6 +29,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.name" + isDraggable={isDraggable} value={packageName} iconType="document" /> @@ -37,6 +39,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.version" + isDraggable={isDraggable} value={packageVersion} iconType="document" /> @@ -46,6 +49,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.summary" + isDraggable={isDraggable} value={packageSummary} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx index 7cff1166cd0de1..b61e00f1752b80 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -242,6 +242,7 @@ describe('UserHostWorkingDir', () => { { ); - expect(wrapper.find('[data-test-subj="draggable-content-user.domain"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-user.domain"]').exists()).toBe(true); }); test('it renders a draggable with an overridden field name when userDomain is provided, and userDomainField is also specified as a prop', () => { @@ -261,6 +262,7 @@ describe('UserHostWorkingDir', () => { { ); - expect( - wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() - ).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-overridden.field.name"]').exists()).toBe( + true + ); }); test('it renders a draggable `user.name` field (by default) when userName is provided, and userNameField is NOT specified as a prop', () => { @@ -283,6 +285,7 @@ describe('UserHostWorkingDir', () => { { ); - expect(wrapper.find('[data-test-subj="draggable-content-user.name"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-user.name"]').exists()).toBe(true); }); test('it renders a draggable with an overridden field name when userName is provided, and userNameField is also specified as a prop', () => { @@ -302,6 +305,7 @@ describe('UserHostWorkingDir', () => { { ); - expect( - wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() - ).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-overridden.field.name"]').exists()).toBe( + true + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx index 0ab3624970c288..9e789cbd7aba2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx @@ -14,12 +14,13 @@ import { HostWorkingDir } from './host_working_dir'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; + hostName: string | null | undefined; + hostNameSeparator?: string; userDomain: string | null | undefined; userDomainField?: string; userName: string | null | undefined; userNameField?: string; - hostName: string | null | undefined; - hostNameSeparator?: string; workingDirectory: string | null | undefined; } @@ -29,6 +30,7 @@ export const UserHostWorkingDir = React.memo( eventId, hostName, hostNameSeparator = '@', + isDraggable, userDomain, userDomainField = 'user.domain', userName, @@ -42,6 +44,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} field={userNameField} + isDraggable={isDraggable} value={userName} iconType="user" /> @@ -61,6 +64,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} field={userDomainField} + isDraggable={isDraggable} value={userDomain} /> @@ -76,6 +80,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} hostName={hostName} + isDraggable={isDraggable} workingDirectory={workingDirectory} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 6c59df606cd36b..94cbe43e93d2d7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -525,6 +525,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx index 7b44862040f2d5..a4dbde1a5626dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx @@ -24,15 +24,16 @@ Details.displayName = 'Details'; interface ZeekDetailsProps { browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const ZeekDetails = React.memo(({ data, timelineId }) => +export const ZeekDetails = React.memo(({ data, isDraggable, timelineId }) => data.zeek != null ? (
- + - +
) : null ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 6b154d4d327075..12f2fd08163ba9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -44,6 +44,7 @@ describe('zeek_row_renderer', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonZeek, + isDraggable: true, timelineId: 'test', }); @@ -63,6 +64,7 @@ describe('zeek_row_renderer', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( 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 2b6311b8cae83a..0a265fa7522b13 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 @@ -21,9 +21,14 @@ export const zeekRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( - + ), }; 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 e4d5a6a86682d8..412fd9d04fe7c1 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 @@ -67,9 +67,10 @@ export const sha1StringRenderer: StringRenderer = (value: string) => export const DraggableZeekElement = React.memo<{ id: string; field: string; + isDraggable?: boolean; value: string | null | undefined; stringRenderer?: StringRenderer; -}>(({ id, field, value, stringRenderer = defaultStringRenderer }) => { +}>(({ id, field, isDraggable, value, stringRenderer = defaultStringRenderer }) => { const dataProviderProp = useMemo( () => ({ and: [], @@ -105,7 +106,7 @@ export const DraggableZeekElement = React.memo<{ return value != null ? ( - + ) : null; }); @@ -203,10 +204,11 @@ export const constructDroppedValue = (dropped: boolean | null | undefined): stri interface ZeekSignatureProps { data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const ZeekSignature = React.memo(({ data, timelineId }) => { +export const ZeekSignature = React.memo(({ data, isDraggable, timelineId }) => { const id = `zeek-signature-draggable-zeek-element-${timelineId}-${data._id}`; const sessionId: string | null | undefined = get('zeek.session_id[0]', data); const dataSet: string | null | undefined = get('event.dataset[0]', data); @@ -234,42 +236,92 @@ export const ZeekSignature = React.memo(({ data, timelineId return ( <> - + - - - - - - - - + + + + + + + + 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 a68617536c6afe..ef47b474350c76 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 @@ -5,7 +5,6 @@ * 2.0. */ -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { EqlOptionsSelected } from '../../../../common/search_strategy/timeline'; import type { TimelineEventsType, @@ -26,8 +25,6 @@ export type TimelineModel = TGridModelForTimeline & { prevActiveTab: TimelineTabs; /** Timeline saved object owner */ createdBy?: string; - /** The sources of the event data shown in the timeline */ - dataProviders: DataProvider[]; /** A summary of the events and notes in this timeline */ description: string; eqlOptions: EqlOptionsSelected; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh index 6c4047552ecd7c..0aa6eeb04c28e6 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh @@ -14,5 +14,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/actions/list_action_types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/actions/connector_types \ | jq . diff --git a/x-pack/plugins/timelines/common/types/timeline/rows/index.ts b/x-pack/plugins/timelines/common/types/timeline/rows/index.ts index b598d132737981..089f886e1c2216 100644 --- a/x-pack/plugins/timelines/common/types/timeline/rows/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/rows/index.ts @@ -15,10 +15,12 @@ export interface RowRenderer { renderRow: ({ browserFields, data, + isDraggable, timelineId, }: { browserFields: BrowserFields; data: Ecs; + isDraggable: boolean; timelineId: string; }) => React.ReactNode; } diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx index eb9c95f0998c62..dd5ef27c32a89c 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx @@ -5,18 +5,20 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { DraggableId } from 'react-beautiful-dnd'; +import { useDispatch } from 'react-redux'; + +import { isEmpty } from 'lodash'; +import { DataProvider, stopPropagationAndPreventDefault, TimelineId } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; import { HoverActionComponentProps } from './types'; - -const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', { - defaultMessage: 'Add to timeline investigation', -}); +import { tGridActions } from '../../..'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import * as i18n from './translations'; export const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a'; @@ -25,7 +27,7 @@ export interface UseGetHandleStartDragToTimelineArgs { draggableId: DraggableId | undefined; } -export const useGetHandleStartDragToTimeline = ({ +const useGetHandleStartDragToTimeline = ({ field, draggableId, }: UseGetHandleStartDragToTimelineArgs): (() => void) => { @@ -41,8 +43,59 @@ export const useGetHandleStartDragToTimeline = ({ return handleStartDragToTimeline; }; -export const AddToTimelineButton: React.FC = React.memo( - ({ field, onClick, ownFocus, showTooltip = false, value }) => { +export interface AddToTimelineButtonProps extends HoverActionComponentProps { + draggableId?: DraggableId; + dataProvider?: DataProvider[] | DataProvider; +} + +const AddToTimelineButton: React.FC = React.memo( + ({ + closePopOver, + dataProvider, + defaultFocusedButtonRef, + draggableId, + field, + keyboardEvent, + ownFocus, + showTooltip = false, + value, + }) => { + const dispatch = useDispatch(); + const { addSuccess } = useAppToasts(); + const startDragToTimeline = useGetHandleStartDragToTimeline({ draggableId, field }); + const handleStartDragToTimeline = useCallback(() => { + if (draggableId != null) { + startDragToTimeline(); + } else if (!isEmpty(dataProvider)) { + const addDataProvider = Array.isArray(dataProvider) ? dataProvider : [dataProvider]; + addDataProvider.forEach((provider) => { + if (provider) { + dispatch( + tGridActions.addProviderToTimeline({ + id: TimelineId.active, + dataProvider: provider, + }) + ); + addSuccess(i18n.ADDED_TO_TIMELINE_MESSAGE(provider.name)); + } + }); + } + + if (closePopOver != null) { + closePopOver(); + } + }, [addSuccess, closePopOver, dataProvider, dispatch, draggableId, startDragToTimeline]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === ADD_TO_TIMELINE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + handleStartDragToTimeline(); + } + }, [handleStartDragToTimeline, keyboardEvent, ownFocus]); + return showTooltip ? ( = React.me field, value, })} - content={ADD_TO_TIMELINE} + content={i18n.ADD_TO_TIMELINE} shortcut={ADD_TO_TIMELINE_KEYBOARD_SHORTCUT} showShortcut={ownFocus} /> } > ) : ( ); } ); AddToTimelineButton.displayName = 'AddToTimelineButton'; + +// eslint-disable-next-line import/no-default-export +export { AddToTimelineButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx index 52d8fb439526f8..d59383b8553ea9 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers'; @@ -30,28 +32,48 @@ export const NESTED_COLUMN = (field: string) => export const COLUMN_TOGGLE_KEYBOARD_SHORTCUT = 'i'; -export interface ColumnToggleFnArgs { - toggleColumn: (column: ColumnHeaderOptions) => void; - field: string; -} - -export const columnToggleFn = ({ toggleColumn, field }: ColumnToggleFnArgs): void => { - return toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }); -}; - export interface ColumnToggleProps extends HoverActionComponentProps { isDisabled: boolean; isObjectArray: boolean; + toggleColumn: (column: ColumnHeaderOptions) => void; } -export const ColumnToggleButton: React.FC = React.memo( - ({ field, isDisabled, isObjectArray, onClick, ownFocus, showTooltip = false, value }) => { +const ColumnToggleButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + isDisabled, + isObjectArray, + keyboardEvent, + ownFocus, + showTooltip = false, + toggleColumn, + value, + }) => { const label = isObjectArray ? NESTED_COLUMN(field) : COLUMN_TOGGLE(field); + const handleToggleColumn = useCallback(() => { + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }); + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, toggleColumn]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === COLUMN_TOGGLE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + handleToggleColumn(); + } + }, [handleToggleColumn, keyboardEvent, ownFocus]); + return showTooltip ? ( = React.memo( > = React.memo( id={field} iconSize="s" iconType="listAdd" - onClick={onClick} + onClick={handleToggleColumn} /> ) : ( = React.memo( id={field} iconSize="s" iconType="listAdd" - onClick={onClick} + onClick={handleToggleColumn} /> ); } ); ColumnToggleButton.displayName = 'ColumnToggleButton'; + +// eslint-disable-next-line import/no-default-export +export { ColumnToggleButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx index 33cc71e12c46f9..1b567dee50683e 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { stopPropagationAndPreventDefault } from '../../../../common'; import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; import { HoverActionComponentProps } from './types'; +import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../clipboard'; export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { defaultMessage: 'Field', @@ -16,22 +18,45 @@ export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; -export type CopyProps = Omit & { +export interface CopyProps extends HoverActionComponentProps { isHoverAction?: boolean; -}; +} -export const CopyButton: React.FC = React.memo( - ({ field, isHoverAction, ownFocus, value }) => { +const CopyButton: React.FC = React.memo( + ({ closePopOver, field, isHoverAction, keyboardEvent, ownFocus, value }) => { + const panelRef = useRef(null); + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + const copyToClipboardButton = panelRef.current?.querySelector( + `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` + ); + if (copyToClipboardButton != null) { + copyToClipboardButton.click(); + } + if (closePopOver != null) { + closePopOver(); + } + } + }, [closePopOver, keyboardEvent, ownFocus]); return ( - +
+ +
); } ); CopyButton.displayName = 'CopyButton'; + +// eslint-disable-next-line import/no-default-export +export { CopyButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx index 421cd0089c1b76..58f7b4a831e513 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiButtonIconPropsForButton, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; @@ -17,34 +19,50 @@ export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.fil }); export const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f'; -export const filterForValueFn = ({ - field, - value, - filterManager, - onFilterAdded, -}: FilterValueFnArgs): void => { - const makeFilter = (currentVal: string | null | undefined) => - currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal); - const filters = Array.isArray(value) - ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) - : makeFilter(value); +export type FilterForValueProps = HoverActionComponentProps & FilterValueFnArgs; - const activeFilterManager = filterManager; +const FilterForValueButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent, + onFilterAdded, + ownFocus, + showTooltip = false, + value, + }) => { + const filterForValueFn = useCallback(() => { + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); - if (activeFilterManager != null) { - activeFilterManager.addFilters(filters); - if (onFilterAdded != null) { - onFilterAdded(); - } - } -}; + const activeFilterManager = filterManager; -export interface FilterForValueProps extends HoverActionComponentProps { - defaultFocusedButtonRef: EuiButtonIconPropsForButton['buttonRef']; -} + if (activeFilterManager != null) { + activeFilterManager.addFilters(filters); + if (onFilterAdded != null) { + onFilterAdded(); + } + } + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, filterManager, onFilterAdded, value]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === FILTER_FOR_VALUE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + filterForValueFn(); + } + }, [filterForValueFn, keyboardEvent, ownFocus]); -export const FilterForValueButton: React.FC = React.memo( - ({ defaultFocusedButtonRef, field, onClick, ownFocus, showTooltip = false, value }) => { return showTooltip ? ( = React.memo( data-test-subj="filter-for-value" iconSize="s" iconType="plusInCircle" - onClick={onClick} + onClick={filterForValueFn} /> ) : ( @@ -77,10 +95,13 @@ export const FilterForValueButton: React.FC = React.memo( data-test-subj="filter-for-value" iconSize="s" iconType="plusInCircle" - onClick={onClick} + onClick={filterForValueFn} /> ); } ); FilterForValueButton.displayName = 'FilterForValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterForValueButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx index bfa7848025bf47..03150d6371397b 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; @@ -18,32 +20,50 @@ export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.fil export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o'; -export const filterOutValueFn = ({ - field, - value, - filterManager, - onFilterAdded, -}: FilterValueFnArgs) => { - const makeFilter = (currentVal: string | null | undefined) => - currentVal?.length === 0 - ? createFilter(field, null, false) - : createFilter(field, currentVal, true); - const filters = Array.isArray(value) - ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) - : makeFilter(value); +const FilterOutValueButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent, + onFilterAdded, + ownFocus, + showTooltip = false, + value, + }) => { + const filterOutValueFn = useCallback(() => { + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 + ? createFilter(field, null, false) + : createFilter(field, currentVal, true); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); - const activeFilterManager = filterManager; + const activeFilterManager = filterManager; - if (activeFilterManager != null) { - activeFilterManager.addFilters(filters); - if (onFilterAdded != null) { - onFilterAdded(); - } - } -}; + if (activeFilterManager != null) { + activeFilterManager.addFilters(filters); + if (onFilterAdded != null) { + onFilterAdded(); + } + } + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, filterManager, onFilterAdded, value]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === FILTER_OUT_VALUE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + filterOutValueFn(); + } + }, [filterOutValueFn, keyboardEvent, ownFocus]); -export const FilterOutValueButton: React.FC = React.memo( - ({ field, onClick, ownFocus, showTooltip = false, value }) => { return showTooltip ? ( = React.m > ) : ( ); } ); FilterOutValueButton.displayName = 'FilterOutValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterOutValueButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx new file mode 100644 index 00000000000000..2f8587ddfab497 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', { + defaultMessage: 'Add to timeline investigation', +}); + +export const ADDED_TO_TIMELINE_MESSAGE = (fieldOrValue: string) => + i18n.translate('xpack.timelines.hoverActions.addToTimeline.addedFieldMessage', { + values: { fieldOrValue }, + defaultMessage: `Added {fieldOrValue} to timeline`, + }); diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts index 4999638e0fe817..fdef1403e3dc2f 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { EuiButtonIconPropsForButton } from '@elastic/eui'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; export interface FilterValueFnArgs { @@ -14,8 +16,10 @@ export interface FilterValueFnArgs { } export interface HoverActionComponentProps { + closePopOver?: () => void; + defaultFocusedButtonRef?: EuiButtonIconPropsForButton['buttonRef']; field: string; - onClick?: () => void; + keyboardEvent?: React.KeyboardEvent; ownFocus: boolean; showTooltip?: boolean; value?: string[] | string | null; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx index 2329134d85626c..fc8fcfa488a76e 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx @@ -4,86 +4,83 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { - AddToTimelineButton, - ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, - UseGetHandleStartDragToTimelineArgs, - useGetHandleStartDragToTimeline, -} from './actions/add_to_timeline'; -import { - ColumnToggleButton, - columnToggleFn, - ColumnToggleFnArgs, - ColumnToggleProps, - COLUMN_TOGGLE_KEYBOARD_SHORTCUT, -} from './actions/column_toggle'; -import { CopyButton, CopyProps, COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT } from './actions/copy'; -import { - FilterForValueButton, - filterForValueFn, - FilterForValueProps, - FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, -} from './actions/filter_for_value'; -import { - FilterOutValueButton, - filterOutValueFn, - FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, -} from './actions/filter_out_value'; -import { HoverActionComponentProps, FilterValueFnArgs } from './actions/types'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import React, { ReactElement } from 'react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import type { AddToTimelineButtonProps } from './actions/add_to_timeline'; +import type { ColumnToggleProps } from './actions/column_toggle'; +import type { CopyProps } from './actions/copy'; +import type { HoverActionComponentProps, FilterValueFnArgs } from './actions/types'; export interface HoverActionsConfig { - addToTimeline: { - AddToTimelineButton: React.FC; - keyboardShortcut: string; - useGetHandleStartDragToTimeline: (args: UseGetHandleStartDragToTimelineArgs) => () => void; - }; - columnToggle: { - ColumnToggleButton: React.FC; - columnToggleFn: (args: ColumnToggleFnArgs) => void; - keyboardShortcut: string; - }; - copy: { - CopyButton: React.FC; - keyboardShortcut: string; - }; - filterForValue: { - FilterForValueButton: React.FC; - filterForValueFn: (args: FilterValueFnArgs) => void; - keyboardShortcut: string; - }; - filterOutValue: { - FilterOutValueButton: React.FC; - filterOutValueFn: (args: FilterValueFnArgs) => void; - keyboardShortcut: string; - }; + getAddToTimelineButton: ( + props: AddToTimelineButtonProps + ) => ReactElement; + getColumnToggleButton: (props: ColumnToggleProps) => ReactElement; + getCopyButton: (props: CopyProps) => ReactElement; + getFilterForValueButton: ( + props: HoverActionComponentProps & FilterValueFnArgs + ) => ReactElement; + getFilterOutValueButton: ( + props: HoverActionComponentProps & FilterValueFnArgs + ) => ReactElement; } -export const addToTimeline = { - AddToTimelineButton, - keyboardShortcut: ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, - useGetHandleStartDragToTimeline, +const AddToTimelineButtonLazy = React.lazy(() => import('./actions/add_to_timeline')); +const getAddToTimelineButtonLazy = (store: Store, props: AddToTimelineButtonProps) => { + return ( + }> + + + + + + + ); }; -export const columnToggle = { - ColumnToggleButton, - columnToggleFn, - keyboardShortcut: COLUMN_TOGGLE_KEYBOARD_SHORTCUT, +const ColumnToggleButtonLazy = React.lazy(() => import('./actions/column_toggle')); +const getColumnToggleButtonLazy = (props: ColumnToggleProps) => { + return ( + }> + + + ); }; -export const copy = { - CopyButton, - keyboardShortcut: COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT, +const CopyButtonLazy = React.lazy(() => import('./actions/copy')); +const getCopyButtonLazy = (props: CopyProps) => { + return ( + }> + + + ); }; -export const filterForValue = { - FilterForValueButton, - filterForValueFn, - keyboardShortcut: FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, +const FilterForValueButtonLazy = React.lazy(() => import('./actions/filter_for_value')); +const getFilterForValueButtonLazy = (props: HoverActionComponentProps & FilterValueFnArgs) => { + return ( + }> + + + ); }; -export const filterOutValue = { - FilterOutValueButton, - filterOutValueFn, - keyboardShortcut: FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, +const FilterOutValueButtonLazy = React.lazy(() => import('./actions/filter_out_value')); +const getFilterOutValueButtonLazy = (props: HoverActionComponentProps & FilterValueFnArgs) => { + return ( + }> + + + ); }; + +export const getHoverActions = (store?: Store): HoverActionsConfig => ({ + getAddToTimelineButton: getAddToTimelineButtonLazy.bind(null, store!), + getColumnToggleButton: getColumnToggleButtonLazy, + getCopyButton: getCopyButtonLazy, + getFilterForValueButton: getFilterForValueButtonLazy, + getFilterOutValueButton: getFilterOutValueButtonLazy, +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx index 65762b93cd43f9..0d606ad28eff2d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx @@ -80,6 +80,7 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, + isDraggable: false, timelineId, })}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx index 5cd709d2de3c77..1a4bfcb0e4ab5a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx @@ -23,6 +23,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: false, timelineId: 'test', }); const wrapper = shallow({children}); @@ -37,6 +38,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: false, timelineId: 'test', }); const wrapper = mount({children}); diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts index f7d3297738373d..610d1b26f23518 100644 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -17,6 +17,7 @@ export const mockGlobalState: TimelineState = { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z', }, + dataProviders: [], deletedEventIds: [], excludedRowRendererIds: [], expandedDetail: {}, diff --git a/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx index 52ea1fa8271369..5a8afb2036abf2 100644 --- a/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx +++ b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx @@ -8,28 +8,9 @@ import React from 'react'; /* eslint-disable react/display-name */ export const mockHoverActions = { - addToTimeline: { - AddToTimelineButton: () => <>{'Add To Timeline'}, - keyboardShortcut: 'timelineAddShortcut', - useGetHandleStartDragToTimeline: () => jest.fn, - }, - columnToggle: { - ColumnToggleButton: () => <>{'Column Toggle'}, - columnToggleFn: jest.fn, - keyboardShortcut: 'columnToggleShortcut', - }, - copy: { - CopyButton: () => <>{'Copy button'}, - keyboardShortcut: 'copyShortcut', - }, - filterForValue: { - FilterForValueButton: () => <>{'Filter button'}, - filterForValueFn: jest.fn, - keyboardShortcut: 'filterForShortcut', - }, - filterOutValue: { - FilterOutValueButton: () => <>{'Filter out button'}, - filterOutValueFn: jest.fn, - keyboardShortcut: 'filterOutShortcut', - }, + getAddToTimelineButton: () => <>{'Add To Timeline'}, + getColumnToggleButton: () => <>{'Column Toggle'}, + getCopyButton: () => <>{'Copy button'}, + getFilterForValueButton: () => <>{'Filter button'}, + getFilterOutValueButton: () => <>{'Filter out button'}, }; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts index 31b6ea9e665ac1..56631c498c755f 100644 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -1549,6 +1549,7 @@ export const mockTgridModel: TGridModel = { initialWidth: 180, }, ], + dataProviders: [], defaultColumns: [], queryFields: [], dateRange: { diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 29d84331cffaa5..cb931ff53d4455 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -26,7 +26,7 @@ import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './type import { tGridReducer } from './store/t_grid/reducer'; import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; -import * as hoverActions from './components/hover_actions'; +import { getHoverActions } from './components/hover_actions'; export class TimelinesPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private _store: Store | undefined; @@ -41,7 +41,7 @@ export class TimelinesPlugin implements Plugin { } return { getHoverActions: () => { - return hoverActions; + return getHoverActions(this._store!); }, getTGrid: (props: TGridProps) => { return getTGridLazy(props, { diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts index 6d9e9e5bc73794..64c4d8a78c7acc 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -9,6 +9,7 @@ import actionCreatorFactory from 'typescript-fsa'; import type { TimelineNonEcsData } from '../../../common/search_strategy'; import type { ColumnHeaderOptions, + DataProvider, SortColumnTimeline, TimelineExpandedDetailType, } from '../../../common/types/timeline'; @@ -100,3 +101,7 @@ export const initializeTGridSettings = actionCreator('I export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean }>( 'SET_TGRID_SELECT_ALL' ); + +export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>( + 'ADD_PROVIDER_TO_TIMELINE' +); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts index 8bcf246dadb03b..dd056f1e9237ab 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts @@ -12,7 +12,11 @@ import type { ToggleDetailPanel } from './actions'; import { TGridPersistInput, TimelineById, TimelineId } from './types'; import type { TGridModel, TGridModelSettings } from './model'; -import type { ColumnHeaderOptions, SortColumnTimeline } from '../../../common/types/timeline'; +import type { + ColumnHeaderOptions, + DataProvider, + SortColumnTimeline, +} from '../../../common/types/timeline'; import { getTGridManageDefaults, tGridDefaults } from './defaults'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -421,3 +425,35 @@ export const updateTimelineDetailsPanel = (action: ToggleDetailPanel) => { [expandedTabType]: {}, }; }; + +export const addProviderToTimelineHelper = ( + id: string, + provider: DataProvider, + timelineById: TimelineById +): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.dataProviders.findIndex((p) => p.id === provider.id); + + if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { + provider.id = `${provider.id}-${ + timeline.dataProviders.filter((p) => p.id === provider.id).length + }`; + } + + const dataProviders = + alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) + ? [ + ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), + provider, + ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), + ] + : [...timeline.dataProviders, provider]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts index 2d4f9f0fca35f7..4ed4448f4cf352 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -10,6 +10,7 @@ import type { Filter, FilterManager } from '../../../../../../src/plugins/data/p import type { TimelineNonEcsData } from '../../../common/search_strategy'; import type { ColumnHeaderOptions, + DataProvider, TimelineExpandedDetail, SortColumnTimeline, SerializedFilterQuery, @@ -39,6 +40,8 @@ export interface TGridModel extends TGridModelSettings { Pick & ColumnHeaderOptions >; + /** The sources of the event data shown in the timeline */ + dataProviders: DataProvider[]; /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ dateRange: { start: string; @@ -81,6 +84,7 @@ export interface TGridModel extends TGridModelSettings { export type TGridModelForTimeline = Pick< TGridModel, | 'columns' + | 'dataProviders' | 'dateRange' | 'deletedEventIds' | 'excludedRowRendererIds' diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts index 57c45f857554d3..751837691ea105 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -7,6 +7,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { + addProviderToTimeline, applyDeltaToColumnWidth, clearEventsDeleted, clearEventsLoading, @@ -28,6 +29,7 @@ import { } from './actions'; import { + addProviderToTimelineHelper, applyDeltaToTimelineColumnWidth, createInitTGrid, setInitializeTgridSettings, @@ -209,4 +211,8 @@ export const tGridReducer = reducerWithInitialState(initialTGridState) }, }, })) + .case(addProviderToTimeline, (state, { id, dataProvider }) => ({ + ...state, + timelineById: addProviderToTimelineHelper(id, dataProvider, state.timelineById), + })) .build(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 07f4072e02b851..dff2ba17cb3f0f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -247,6 +247,7 @@ export const ExpandedRow: FC = ({ item }) => { onTabClick={() => {}} expand={false} style={{ width: '100%' }} + data-test-subj="transformExpandedRowTabbedContent" /> ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx index 7794d65934d6da..d9ee384f3ec699 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx @@ -9,7 +9,12 @@ import React from 'react'; import { EuiBadge, SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TermClause, FieldClause, Value } from './common'; -import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; +import { + TRANSFORM_FUNCTION, + TRANSFORM_MODE, + TRANSFORM_STATE, +} from '../../../../../../common/constants'; +import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform'; import { TransformListRow } from '../../../../common'; import { getTaskStateBadge } from './use_columns'; @@ -93,7 +98,20 @@ export const filterTransforms = ( // the status value is an array of string(s) e.g. ['failed', 'stopped'] ts = transforms.filter((transform) => (c.value as Value[]).includes(transform.stats.state)); } else { - ts = transforms.filter((transform) => transform.mode === c.value); + ts = transforms.filter((transform) => { + if (c.field === 'mode') { + return transform.mode === c.value; + } + if (c.field === 'type') { + if (c.value === TRANSFORM_FUNCTION.PIVOT) { + return isPivotTransform(transform.config); + } + if (c.value === TRANSFORM_FUNCTION.LATEST) { + return isLatestTransform(transform.config); + } + } + return false; + }); } } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index f3974430b662c7..af2325ede2021f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -20,13 +20,14 @@ describe('Transform: Job List Columns', () => { const columns: ReturnType['columns'] = result.current.columns; - expect(columns).toHaveLength(7); + expect(columns).toHaveLength(8); expect(columns[0].isExpander).toBeTruthy(); expect(columns[1].name).toBe('ID'); expect(columns[2].name).toBe('Description'); - expect(columns[3].name).toBe('Status'); - expect(columns[4].name).toBe('Mode'); - expect(columns[5].name).toBe('Progress'); - expect(columns[6].name).toBe('Actions'); + expect(columns[3].name).toBe('Type'); + expect(columns[4].name).toBe('Status'); + expect(columns[5].name).toBe('Mode'); + expect(columns[6].name).toBe('Progress'); + expect(columns[7].name).toBe('Actions'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index e186acf31d34f1..dbdd3409c7e342 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -23,7 +23,11 @@ import { RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { TransformId } from '../../../../../../common/types/transform'; +import { + isLatestTransform, + isPivotTransform, + TransformId, +} from '../../../../../../common/types/transform'; import { TransformStats } from '../../../../../../common/types/transform_stats'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; @@ -95,6 +99,7 @@ export const useColumns = ( EuiTableComputedColumnType, EuiTableComputedColumnType, EuiTableComputedColumnType, + EuiTableComputedColumnType, EuiTableActionsColumnType ] = [ { @@ -145,6 +150,27 @@ export const useColumns = ( sortable: true, truncateText: true, }, + { + name: i18n.translate('xpack.transform.type', { defaultMessage: 'Type' }), + 'data-test-subj': 'transformListColumnType', + sortable: (item: TransformListRow) => item.mode, + truncateText: true, + render(item: TransformListRow) { + let transformType = i18n.translate('xpack.transform.type.unknown', { + defaultMessage: 'unknown', + }); + if (isPivotTransform(item.config) === true) { + transformType = i18n.translate('xpack.transform.type.pivot', { defaultMessage: 'pivot' }); + } + if (isLatestTransform(item.config) === true) { + transformType = i18n.translate('xpack.transform.type.latest', { + defaultMessage: 'latest', + }); + } + return {transformType}; + }, + width: '100px', + }, { name: i18n.translate('xpack.transform.status', { defaultMessage: 'Status' }), 'data-test-subj': 'transformListColumnStatus', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 72b3e14bd55bee..0e8a0d8e45a7d1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19120,9 +19120,6 @@ "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) はElasticSearchの{ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}) を超えています。ElasticSearchで一致する{ES_MAX_SIZE_BYTES_PATH}を設定してください。あるいは、Kibanaでxpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}を低くしてください。", "xpack.reporting.diagnostic.noUsableSandbox": "Chromiumサンドボックスを使用できません。これは「xpack.reporting.capture.browser.chromium.disableSandbox」で無効にすることができます。この作業はご自身の責任で行ってください。{url}を参照してください", "xpack.reporting.diagnostic.screenshotFailureMessage": "Kibanaインストールのスクリーンショットを作成できませんでした。", - "xpack.reporting.errorButton.showReportErrorAriaLabel": "レポートエラーを表示", - "xpack.reporting.errorButton.unableToFetchReportContentTitle": "レポートのコンテンツを取得できません", - "xpack.reporting.errorButton.unableToGenerateReportTitle": "レポートを生成できません", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana の高度な設定「{dateFormatTimezone}」が「ブラウザー」に設定されています。あいまいさを避けるために日付は UTC 形式に変換されます。", @@ -19133,7 +19130,6 @@ "xpack.reporting.exportTypes.printablePdf.logoDescription": "Elastic 提供", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "{pageCount} ページ中 {currentPage} ページ目", "xpack.reporting.jobsQuery.deleteError": "レポートを削除できません:{error}", - "xpack.reporting.jobStatuses.cancelledText": "キャンセル済み", "xpack.reporting.jobStatuses.completedText": "完了", "xpack.reporting.jobStatuses.failedText": "失敗", "xpack.reporting.jobStatuses.pendingText": "保留中", @@ -19159,7 +19155,6 @@ "xpack.reporting.listing.reports.subtitle": "Kibanaアプリケーションで生成されたレポートを取得します。", "xpack.reporting.listing.reportstitle": "レポート", "xpack.reporting.listing.table.captionDescription": "Kibanaアプリケーションでレポートが生成されました", - "xpack.reporting.listing.table.csvContainsFormulas": "CSVには、スプレッドシートアプリケーションで式と解釈される可能性のある文字が含まれています。", "xpack.reporting.listing.table.deleteCancelButton": "キャンセル", "xpack.reporting.listing.table.deleteConfim": "{reportTitle} レポートを削除しました", "xpack.reporting.listing.table.deleteConfirmButton": "削除", @@ -19171,17 +19166,12 @@ "xpack.reporting.listing.table.downloadReport": "レポートをダウンロード", "xpack.reporting.listing.table.downloadReportAriaLabel": "レポートをダウンロード", "xpack.reporting.listing.table.loadingReportsDescription": "レポートを読み込み中です", - "xpack.reporting.listing.table.maxSizeReachedTooltip": "最大サイズに達成、部分データが含まれています。", "xpack.reporting.listing.table.noCreatedReportsDescription": "レポートが作成されていません", "xpack.reporting.listing.table.requestFailedErrorMessage": "リクエストに失敗しました", "xpack.reporting.listing.tableColumns.actionsTitle": "アクション", "xpack.reporting.listing.tableColumns.createdAtTitle": "作成日時:", "xpack.reporting.listing.tableColumns.reportTitle": "レポート", "xpack.reporting.listing.tableColumns.statusTitle": "ステータス", - "xpack.reporting.listing.tableValue.statusDetail.maxSizeReachedText": " - 最大サイズに達成", - "xpack.reporting.listing.tableValue.statusDetail.pendingStatusReachedText": "保留中 - ジョブの処理持ち", - "xpack.reporting.listing.tableValue.statusDetail.statusTimestampText": "{statusTimestamp} 時点で {statusLabel}", - "xpack.reporting.listing.tableValue.statusDetail.warningsText": "エラー発生:詳細はジョブ情報をご覧ください。", "xpack.reporting.management.reportingTitle": "レポート", "xpack.reporting.panelContent.advancedOptions": "高度なオプション", "xpack.reporting.panelContent.copyUrlButtonLabel": "POST URL をコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index aad31db65b19a4..97302468aae47c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19550,9 +19550,6 @@ "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) 大于 ElasticSearch 的 {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes})。请在 ElasticSearch 中将 {ES_MAX_SIZE_BYTES_PATH} 设置为匹配或减小 Kibana 中的 xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}。", "xpack.reporting.diagnostic.noUsableSandbox": "无法使用 Chromium 沙盒。您自行承担使用“xpack.reporting.capture.browser.chromium.disableSandbox”禁用此项的风险。请参见 {url}", "xpack.reporting.diagnostic.screenshotFailureMessage": "我们无法拍摄 Kibana 安装的屏幕截图。", - "xpack.reporting.errorButton.showReportErrorAriaLabel": "显示报告错误", - "xpack.reporting.errorButton.unableToFetchReportContentTitle": "无法提取报告内容", - "xpack.reporting.errorButton.unableToGenerateReportTitle": "无法生成报告", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana 高级设置“{dateFormatTimezone}”已设置为“浏览器”。日期将格式化为 UTC 以避免混淆。", @@ -19563,7 +19560,6 @@ "xpack.reporting.exportTypes.printablePdf.logoDescription": "由 Elastic 提供支持", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "第 {currentPage} 页,共 {pageCount} 页", "xpack.reporting.jobsQuery.deleteError": "无法删除报告:{error}", - "xpack.reporting.jobStatuses.cancelledText": "已取消", "xpack.reporting.jobStatuses.completedText": "已完成", "xpack.reporting.jobStatuses.failedText": "失败", "xpack.reporting.jobStatuses.pendingText": "待处理", @@ -19589,7 +19585,6 @@ "xpack.reporting.listing.reports.subtitle": "获取在 Kibana 应用程序中生成的报告。", "xpack.reporting.listing.reportstitle": "报告", "xpack.reporting.listing.table.captionDescription": "在 Kibana 应用程序中生成的报告", - "xpack.reporting.listing.table.csvContainsFormulas": "您的 CSV 包含电子表格应用程序可解释为公式的字符。", "xpack.reporting.listing.table.deleteCancelButton": "取消", "xpack.reporting.listing.table.deleteConfim": "报告 {reportTitle} 已删除", "xpack.reporting.listing.table.deleteConfirmButton": "删除", @@ -19601,17 +19596,12 @@ "xpack.reporting.listing.table.downloadReport": "下载报告", "xpack.reporting.listing.table.downloadReportAriaLabel": "下载报告", "xpack.reporting.listing.table.loadingReportsDescription": "正在载入报告", - "xpack.reporting.listing.table.maxSizeReachedTooltip": "已达到最大大小,包含部分数据。", "xpack.reporting.listing.table.noCreatedReportsDescription": "未创建任何报告", "xpack.reporting.listing.table.requestFailedErrorMessage": "请求失败", "xpack.reporting.listing.tableColumns.actionsTitle": "操作", "xpack.reporting.listing.tableColumns.createdAtTitle": "创建于", "xpack.reporting.listing.tableColumns.reportTitle": "报告", "xpack.reporting.listing.tableColumns.statusTitle": "状态", - "xpack.reporting.listing.tableValue.statusDetail.maxSizeReachedText": " - 最大大小已达到", - "xpack.reporting.listing.tableValue.statusDetail.pendingStatusReachedText": "待处理 - 正在等候处理作业", - "xpack.reporting.listing.tableValue.statusDetail.statusTimestampText": "{statusTimestamp} 时为 {statusLabel}", - "xpack.reporting.listing.tableValue.statusDetail.warningsText": "发生了错误:请参阅作业信息以了解详情。", "xpack.reporting.management.reportingTitle": "Reporting", "xpack.reporting.panelContent.advancedOptions": "高级选项", "xpack.reporting.panelContent.copyUrlButtonLabel": "复制 POST URL", diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 1403e6c3e52b12..016714d0281716 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -39,7 +39,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": false, }, @@ -146,7 +145,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": true, }, @@ -214,7 +212,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": true, }, diff --git a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts index 11b5d8a51c9a8f..6ed4add217ae74 100644 --- a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { esKuery } from '../../../../../src/plugins/data/public'; +import type { IndexPattern } from '../../../../../src/plugins/data/public'; import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib'; const getKueryString = (urlFilters: string): string => { @@ -25,7 +26,7 @@ const getKueryString = (urlFilters: string): string => { }; export const useUpdateKueryString = ( - indexPattern: IIndexPattern | null, + indexPattern: IndexPattern | null, filterQueryString = '', urlFilters: string ): [string?, Error?] => { diff --git a/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts index 3fb2ada060d657..d16860850bd786 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts @@ -7,10 +7,10 @@ import { handleActions, Action } from 'redux-actions'; import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; -import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import type { IndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; export interface IndexPatternState { - index_pattern: IIndexPattern | null; + index_pattern: IndexPattern | null; errors: any[]; loading: boolean; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts index a76654415c16d5..ec237198809261 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function listActionTypesTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('list_action_types', () => { + describe('connector_types', () => { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 3729b20f82b301..79af6bb279a3de 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -24,6 +24,7 @@ import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { getCreateConnectorUrl } from '../../../../../../../plugins/cases/common/utils/connectors_api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -49,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { const { body: connector } = await supertest - .post('/api/actions/connector') + .post(getCreateConnectorUrl()) .set('kbn-xsrf', 'true') .send({ ...getServiceNowConnector(), diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 10846442d1c84b..e1cf24521d1b1e 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -134,7 +134,8 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet core.savedObjects.registerType({ name: SAVED_OBJECT_WITH_MIGRATION_TYPE, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', // in data.json, we simulate that existing objects were created with `namespaceType: 'single'` + convertToMultiNamespaceTypeVersion: '8.0.0', // in this version we convert from a single-namespace type to a "share-capable" multi-namespace isolated type mappings: { properties: { nonEncryptedAttribute: { @@ -199,6 +200,18 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet }, inputType: typePriorTo790, }), + + // NOTE FOR MAINTAINERS: do not add any more migrations before 8.0.0 unless you regenerate the test data for two of the objects in + // data.json: '362828f0-eef2-11eb-9073-11359682300a' and '36448a90-eef2-11eb-9073-11359682300a. These are used in the test cases 'for + // a saved object that does not need to be migrated before it is converted'. + + // This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its + // new descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` + // in 8.0.0 (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process). + '8.0.0': deps.encryptedSavedObjects.createMigration({ + isMigrationNeededPredicate: (doc): doc is SavedObjectUnsanitizedDoc => true, + migration: (doc) => doc, // no-op + }), }, }); } diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json index 88ec54cdf3a54e..71ac4dfc974d43 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json @@ -223,6 +223,70 @@ } } +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:a67c6950-eed8-11eb-9a62-032b4e4049d1", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "BIOBsx5SjLq3ZQdOJv06XeCAMY9ZrYj8K5bcGa5+wpd3TeT2sqln1+9AGblnfxT7LXRI3sLWQ900+wRQzBhJYx8PNKH+Yw+GdeESpu73PFHdWt/52cJKr+b4EPALFc00tIMEDHdT9FyQhqJ7nV8UpwtjcuTp9SA=", + "nonEncryptedAttribute": "elastic" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.7.0" + }, + "updated_at": "2021-07-27T12:46:23.881Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "saved-object-with-migration:362828f0-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "wWDAtF/5PkCb5BxjfWyRxoIoHbJXlb5cGAKg9ztZ1Bz9Zwo0/xf2yTa3Gq/CbYrvey/F9FZkZOUk03USPaqa5mfFO8FhORkfmNLQaPhgCIDNd6SbIhN8RYkqWVTYSVgcZrwes+VwiTUZ29mCJprVSHwXdyAOy4g=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "mszSQj0+Wv7G6kZJQsqf7CWwjJwwyriMlBcUjSHTLlj+tljbLTb7PI7gR07S9l7BXd3Lquc5PeOJifl2HvnTh8s871d/WdtIvt2K/ggwA2ae9NH6ui8A15cuPlXiGO612qccsIyBzhsftFyWJNuLBApmqeEy7HFe" + }, + "type": "saved-object-with-migration", + "references": [], + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.324Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:36448a90-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "33lfpnBI136UfkdcQLzovzBXdUaeDouN0Z32qkVutgZJ5SU60hMtaHWXNkaU9DGy9jtr0ptwm6FCYmZbyDrlGMwyZP2n0PzMhwW9fRcBh7he12Cm1mImWTrxgYoRtc1MX20/orbINx5VnuNl1Ide7htAm1oPRjM=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "e2rsxBijtMGcdw7A+WAWJNlLOhQCZnEP1sdcHxVO5aQouiUVeI1OTFcOY3h/+iZBlSGvZdGRURgimrSNc0HRicemZx3o4v1gVw0JX3RRatzdl02v3GJoFzBWfQGyf3xhNNWmkweGJrFQqr2kfdKjIHbdVmMt4LZj" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.509Z" + } + } +} + { "type": "doc", "value": { @@ -367,4 +431,4 @@ "updated_at": "2020-06-17T16:29:27.563Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 0b01f4f385da66..311228424afe30 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -527,20 +527,103 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('migrates unencrypted fields on saved objects', async () => { - const { body: decryptedResponse } = await supertest - .get( - `/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` - ) - .expect(200); + function getGetApiUrl({ objectId, spaceId }: { objectId: string; spaceId?: string }) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/${objectId}`; + } + + // For brevity, each encrypted saved object has the same decrypted attributes after migrations/conversion. + // An assertion based on this ensures all encrypted fields can still be decrypted after migrations/conversion have been applied. + const expectedDecryptedAttributes = { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic-migrated', // this field was migrated in 7.8.0 + additionalEncryptedAttribute: 'elastic-migrated-encrypted', // this field was added in 7.9.0 + }; + + // In these test cases, we simulate a scenario where some existing objects that are migrated when Kibana starts up. Note that when a + // document migration is triggered, the saved object "convert" transform is also applied by the Core migration algorithm. + describe('handles index migration correctly', () => { + describe('in the default space', () => { + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '74f3e6d7-b7bb-477d-ac28-92ee22728e6e' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '362828f0-eef2-11eb-9073-11359682300a' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + + describe('in a custom space', () => { + const spaceId = 'custom-space'; + + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: 'a98e22f8-530e-5d69-baf7-97526796f3a6', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is a67c6950-eed8-11eb-9a62-032b4e4049d1 + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: '41395c74-da7a-5679-9535-412d550a6cf7', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is 36448a90-eef2-11eb-9073-11359682300a + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + }); + + // In these test cases, we simulate a scenario where new objects are migrated upon creation. This happens because an outdated + // `migrationVersion` field is included below. Note that when a document migration is triggered, the saved object "convert" transform + // is *not* applied by the Core migration algorithm. + describe('handles document migration correctly', () => { + function getCreateApiUrl({ spaceId }: { spaceId?: string } = {}) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/saved-object-with-migration`; + } + + const objectToCreate = { + attributes: { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic', + }, + migrationVersion: { 'saved-object-with-migration': '7.7.0' }, + }; + + it('in the default space', async () => { + const createApiUrl = getCreateApiUrl(); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; + + const getApiUrl = getGetApiUrl({ objectId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('in a custom space', async () => { + const spaceId = 'custom-space'; + const createApiUrl = getCreateApiUrl({ spaceId }); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; - expect(decryptedResponse.attributes).to.eql({ - // ensures the encrypted field can still be decrypted after the migration - encryptedAttribute: 'this is my secret api key', - // ensures the non-encrypted field has been migrated in 7.8.0 - nonEncryptedAttribute: 'elastic-migrated', - // ensures the non-encrypted field has been migrated into a new encrypted field in 7.9.0 - additionalEncryptedAttribute: 'elastic-migrated-encrypted', + const getApiUrl = getGetApiUrl({ objectId, spaceId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); }); }); }); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index d5aded01fce7b6..5979ae378c22bb 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -21,6 +21,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_maps_by_value')); loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); }); } diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson new file mode 100644 index 00000000000000..9b7ce6ffdd9990 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson @@ -0,0 +1,7 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.1","id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"type":"index-pattern","updated_at":"2021-07-16T18:56:14.524Z","version":"WzM4LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Speaker Count (Area Chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Speaker Count (Area Chart By-Ref)\",\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{\"customLabel\":\"Line Count\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speaker\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Speaker\"},\"schema\":\"segment\"}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Line Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Line Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}}}"},"coreMigrationVersion":"7.12.1","id":"9f90fae0-e66b-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-19T15:32:03.822Z","version":"WzU0NSwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Tag Cloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Tag Cloud\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}"},"coreMigrationVersion":"7.12.1","id":"954e2df0-e66b-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-16T19:25:30.580Z","version":"WzIwMSwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Unique Speakers by Play (Pie chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Unique Speakers by Play (Pie chart By-Ref)\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"params\":{\"field\":\"speaker\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}]}"},"coreMigrationVersion":"7.12.1","id":"6b818220-e66f-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-16T19:52:58.437Z","version":"WzMxMiwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"speaker : \\\"HAMLET\\\" \",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Hamlet Speaking Overtime (Area chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Hamlet Speaking Overtime (Area chart By-Ref)\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speech_number\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}]}"},"coreMigrationVersion":"7.12.1","id":"3b53b990-e8a6-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-19T15:30:22.251Z","version":"WzUxMSwxXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"b05dcb4e-d866-43cd-a6af-d26def3f6231\"},\"panelIndex\":\"b05dcb4e-d866-43cd-a6af-d26def3f6231\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Line Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Line Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{\"customLabel\":\"Line Count\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speaker\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Speaker\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Speaker Count (Area Chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"52747d95-33a3-4c36-b870-4a23f3e4dfec\"},\"panelIndex\":\"52747d95-33a3-4c36-b870-4a23f3e4dfec\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"d7df311d-e0cf-45f2-82cf-80b5a891fa9a\"},\"panelIndex\":\"d7df311d-e0cf-45f2-82cf-80b5a891fa9a\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"Word Cloud\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Words (Tag Cloud By-Ref)\",\"panelRefName\":\"panel_2\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"ab307583-e63e-41fe-adc6-5be5f9b8b053\"},\"panelIndex\":\"ab307583-e63e-41fe-adc6-5be5f9b8b053\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Words (Tag Cloud By-Value)\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"d6be9fb1-7f48-4175-a29a-fb80420cceb9\"},\"panelIndex\":\"d6be9fb1-7f48-4175-a29a-fb80420cceb9\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"params\":{\"field\":\"speaker\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"b8203dae-afe5-4d64-9c13-8ad62206b8e1\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"Shakespeare Search\",\"config\":{\"url\":{\"template\":\"https://shakespeare.folger.edu/search/?search_text={{event.value}}\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"title\":\"Unique Speakers by Play (Pie chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"5098cff8-0d4f-4a71-8d1b-18d19a018b1f\"},\"panelIndex\":\"5098cff8-0d4f-4a71-8d1b-18d19a018b1f\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"910c4965-8781-49b9-9d6d-18ea8241a96a\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"Shakespeare Search\",\"config\":{\"url\":{\"template\":\"https://shakespeare.folger.edu/search/?search_text={{event.value}}\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"2fc385e6-dfe5-49d6-8302-62c17d7a50a4\"},\"panelIndex\":\"2fc385e6-dfe5-49d6-8302-62c17d7a50a4\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speech_number\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"speaker : \\\"HAMLET\\\" \",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Hamlet Speaking Overtime (Area chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"964c5ffe-aae3-40b1-8240-14c5a218bbe2\"},\"panelIndex\":\"964c5ffe-aae3-40b1-8240-14c5a218bbe2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"}]","timeRestore":false,"title":"[7.12.1] Visualize Test Dashboard","version":1},"coreMigrationVersion":"7.12.1","id":"8a8f5a90-e668-11eb-86e8-1ffd09dc5582","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"9f90fae0-e66b-11eb-86e8-1ffd09dc5582","name":"panel_1","type":"visualization"},{"id":"954e2df0-e66b-11eb-86e8-1ffd09dc5582","name":"panel_2","type":"visualization"},{"id":"6b818220-e66f-11eb-86e8-1ffd09dc5582","name":"panel_5","type":"visualization"},{"id":"3b53b990-e8a6-11eb-86e8-1ffd09dc5582","name":"panel_7","type":"visualization"}],"type":"dashboard","updated_at":"2021-07-19T15:49:46.191Z","version":"WzU4NywxXQ=="} +{"exportedCount":6,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts new file mode 100644 index 00000000000000..d3d6ca46cd2273 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'visualize_dashboard_migration_test_7_12_1.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with various Visualize panels from 7.12.1', async () => { + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[7.12.1] Visualize Test Dashboard'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('[7.12.1] Visualize Test Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('should show the edit action for all panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + + // All panels should be editable. This will catch cases where an error does not create an error embeddable. + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + for (const title of panelTitles) { + await dashboardPanelActions.expectExistsEditPanelAction(title); + } + }); + + it('should retain all panel drilldowns from 7.12.1', async () => { + // Both panels configured with drilldowns in 7.12.1 should still have drilldowns. + const totalPanels = await PageObjects.dashboard.getPanelCount(); + let panelsWithDrilldowns = 0; + for (let panelIndex = 0; panelIndex < totalPanels; panelIndex++) { + if ((await PageObjects.dashboard.getPanelDrilldownCount(panelIndex)) === 1) { + panelsWithDrilldowns++; + } + } + expect(panelsWithDrilldowns).to.be(2); + }); + }); +} diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 0b1fce37009862..eb2e339e9be662 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { REPORT_TABLE_ID } from '../../../../plugins/reporting/common/constants'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -37,7 +38,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // to reset the data after deletion testing await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); await pageObjects.common.navigateToApp('reporting'); - await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + await testSubjects.existOrFail(REPORT_TABLE_ID, { timeout: 200000 }); }); after(async () => { @@ -52,7 +53,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Confirm single report deletion works', async () => { log.debug('Checking for reports.'); await retry.try(async () => { - await testSubjects.click('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + await testSubjects.click('checkboxSelectRow-krb7arhe164k0763b50bjm29'); }); const deleteButton = await testSubjects.find('deleteReportButton'); await retry.waitFor('delete button to become enabled', async () => { @@ -62,7 +63,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.exists('confirmModalBodyText'); await testSubjects.click('confirmModalConfirmButton'); await retry.try(async () => { - await testSubjects.waitForDeleted('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + await testSubjects.waitForDeleted('checkboxSelectRow-krb7arhe164k0763b50bjm29'); }); }); @@ -71,10 +72,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const previousButton = await testSubjects.find('pagination-button-previous'); expect(await previousButton.getAttribute('disabled')).to.be('true'); - await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); // find first row of page 1 + await testSubjects.find('checkboxSelectRow-krb7arhe164k0763b50bjm29'); // find first row of page 1 await testSubjects.click('pagination-button-1'); // click page 2 - await testSubjects.find('checkboxSelectRow-k9a9uc4x0gpe1457b16wthc8'); // wait for first row of page 2 + await testSubjects.find('checkboxSelectRow-kraz0qle154g0763b569zz83'); // wait for first row of page 2 await testSubjects.click('pagination-button-2'); // click page 3 await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); // wait for first row of page 3 @@ -82,5 +83,73 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // previous CAN be clicked expect(await previousButton.getAttribute('disabled')).to.be(null); }); + + it('Displays types of report jobs', async () => { + const list = await pageObjects.reporting.getManagementList(); + expectSnapshot(list).toMatchInline(` + Array [ + Object { + "actions": "", + "createdAt": "2021-07-19 @ 10:29 PMtest_user", + "report": "Automated reportsearch", + "status": "Completed at 2021-07-19 @ 10:29 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:47 PMtest_user", + "report": "Discover search [2021-07-19T11:47:35.995-07:00]search", + "status": "Completed at 2021-07-19 @ 06:47 PM", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:46 PMtest_user", + "report": "Discover search [2021-07-19T11:46:00.132-07:00]search", + "status": "Completed at 2021-07-19 @ 06:46 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:44 PMtest_user", + "report": "Discover search [2021-07-19T11:44:48.670-07:00]search", + "status": "Completed at 2021-07-19 @ 06:44 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Pending at 2021-07-19 @ 06:41 PMWaiting for job to be processed.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Failed at 2021-07-19 @ 06:43 PMSee report info for error details.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:41 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:39 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:39 PM", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:38 PM", + }, + ] + `); + }); }); }; diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 027d7027d46eec..0c1dbfd5f826ad 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; + export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects([ 'security', @@ -29,7 +30,9 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('x-pack/test/functional/es_archives/security/discover'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await PageObjects.settings.navigateTo(); }); @@ -87,6 +90,9 @@ export default function ({ getService, getPageObjects }) { after(async function () { await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); }); }); } diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index bb9b0a865ee6b6..84566c1a6f5ff1 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -7,14 +7,17 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; + export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['security', 'settings', 'common', 'accountSetting']); const log = getService('log'); - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('useremail', function () { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/security/discover'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); }); @@ -55,6 +58,9 @@ export default function ({ getService, getPageObjects }) { after(async function () { await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); }); }); } diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index d8867527ba2ac5..a6b3e8e41be999 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -578,6 +578,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, + type: testData.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index e5944c7f125781..b79bef82267d78 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -279,6 +279,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, + type: testData.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts index 68530c586b6e21..c86171cdb1d6f8 100644 --- a/x-pack/test/functional/apps/transform/deleting.ts +++ b/x-pack/test/functional/apps/transform/deleting.ts @@ -24,6 +24,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'batch', progress: 100, }, @@ -35,6 +36,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'continuous', progress: undefined, }, @@ -50,6 +52,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'latest', mode: 'batch', progress: 100, }, @@ -106,6 +109,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.originalConfig.id, { id: testData.originalConfig.id, description: testData.originalConfig.description, + type: testData.expected.row.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 01f93d9e8c0a8f..993c239a043043 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -70,6 +70,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'batch', progress: '100', }, @@ -85,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'latest', mode: 'batch', progress: '100', }, @@ -170,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.originalConfig.id, { id: testData.originalConfig.id, description: testData.transformDescription, + type: testData.expected.row.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 3bced4fca9b401..d50943fad991a4 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - // FLAKY: https://github.com/elastic/kibana/issues/107043 - describe.skip('for user with full transform access', function () { + describe('for user with full transform access', function () { describe('with no data loaded', function () { before(async () => { await transform.securityUI.loginAsTransformPowerUser(); diff --git a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts index 5634ed4736e4d4..6a04d33ff152de 100644 --- a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - // FLAKY: https://github.com/elastic/kibana/issues/107043 - describe.skip('for user with full transform access', function () { + describe('for user with read only transform access', function () { describe('with no data loaded', function () { before(async () => { await transform.securityUI.loginAsTransformViewer(); diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz index 34a30bd84a5929..22423aa1fc99f8 100644 Binary files a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz and b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json index 3c3225a70d47fb..b2363174e92ec5 100644 --- a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json @@ -4,6 +4,561 @@ "aliases": { }, "index": ".reporting-2020.04.19", + "mappings": { + "properties": { + "attempts": { + "type": "long" + }, + "browser_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "completed_at": { + "type": "date" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "jobtype": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "kibana_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "kibana_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "max_attempts": { + "type": "long" + }, + "meta": { + "properties": { + "layout": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "output": { + "properties": { + "content": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "content_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "csv_contains_formulas": { + "type": "boolean" + }, + "max_size_reached": { + "type": "boolean" + }, + "size": { + "type": "long" + } + } + }, + "payload": { + "properties": { + "basePath": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "browserTimezone": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "forceNow": { + "type": "date" + }, + "headers": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexPatternId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexPatternSavedObject": { + "properties": { + "attributes": { + "properties": { + "fieldFormatMap": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timeFieldName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "migrationVersion": { + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "layout": { + "properties": { + "dimensions": { + "properties": { + "height": { + "type": "long" + }, + "width": { + "type": "long" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "metaFields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "relativeUrl": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "relativeUrls": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "searchRequest": { + "properties": { + "body": { + "properties": { + "_source": { + "type": "object" + }, + "docvalue_fields": { + "properties": { + "field": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "query": { + "properties": { + "bool": { + "properties": { + "filter": { + "properties": { + "match_all": { + "type": "object" + }, + "range": { + "properties": { + "order_date": { + "properties": { + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "gte": { + "type": "date" + }, + "lte": { + "type": "date" + } + } + }, + "timestamp": { + "properties": { + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "gte": { + "type": "date" + }, + "lte": { + "type": "date" + } + } + } + } + } + } + } + } + } + } + }, + "script_fields": { + "properties": { + "hour_of_day": { + "properties": { + "script": { + "properties": { + "lang": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "source": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + } + } + }, + "sort": { + "properties": { + "order_date": { + "properties": { + "order": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "unmapped_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "timestamp": { + "properties": { + "order": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "unmapped_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "stored_fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "type": "boolean" + } + } + }, + "index": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "priority": { + "type": "long" + }, + "process_expiration": { + "type": "date" + }, + "started_at": { + "type": "date" + }, + "status": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timeout": { + "type": "long" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".reporting-2021-07-18", "mappings": { "properties": { "attempts": { @@ -55,6 +610,9 @@ } } }, + "migration_version": { + "type": "keyword" + }, "output": { "properties": { "content": { @@ -72,6 +630,15 @@ }, "size": { "type": "long" + }, + "warnings": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -104,4 +671,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/security/discover/data.json.gz b/x-pack/test/functional/es_archives/security/discover/data.json.gz deleted file mode 100644 index cce1558566be5d..00000000000000 Binary files a/x-pack/test/functional/es_archives/security/discover/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/security/discover/mappings.json b/x-pack/test/functional/es_archives/security/discover/mappings.json deleted file mode 100644 index 0f58add932b0c7..00000000000000 --- a/x-pack/test/functional/es_archives/security/discover/mappings.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaceId": { - "type": "keyword" - }, - "timelion-sheet": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json b/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json new file mode 100644 index 00000000000000..a7fc27c8dd2e8e --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json @@ -0,0 +1,50 @@ +{ + "attributes": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "A Saved Search", + "version": 1 + }, + "coreMigrationVersion": "7.15.0", + "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "version": "WzUsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/monitoring_page.ts b/x-pack/test/functional/page_objects/monitoring_page.ts index acd9a443eb7ce3..259af2c917f02e 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.ts +++ b/x-pack/test/functional/page_objects/monitoring_page.ts @@ -17,7 +17,7 @@ export class MonitoringPageObject extends FtrService { } async closeAlertsModal() { - return this.testSubjects.click('alerts-modal-button'); + return this.testSubjects.click('alerts-modal-remind-later-button'); } async clickBreadcrumb(subj: string) { diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 742d41031004bc..302e71304869b3 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -10,6 +10,7 @@ import { format as formatUrl } from 'url'; import supertestAsPromised from 'supertest-as-promised'; import { FtrService } from '../ftr_provider_context'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../../plugins/reporting/common/constants'; export class ReportingPageObject extends FtrService { private readonly browser = this.ctx.getService('browser'); @@ -157,4 +158,21 @@ export class ReportingPageObject extends FtrService { const toTime = 'Sep 23, 1999 @ 18:31:44.000'; await this.timePicker.setAbsoluteRange(fromTime, toTime); } + + async getManagementList() { + const table = await this.testSubjects.find(REPORT_TABLE_ID); + const allRows = await table.findAllByTestSubject(REPORT_TABLE_ROW_ID); + + return await Promise.all( + allRows.map(async (row) => { + const $ = await row.parseDomContent(); + return { + report: $.findTestSubject('reportingListItemObjectTitle').text().trim(), + createdAt: $.findTestSubject('reportJobCreatedAt').text().trim(), + status: $.findTestSubject('reportJobStatus').text().trim(), + actions: $.findTestSubject('reportJobActions').text().trim(), + }; + }) + ); + } } diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 8ff8b58f44caf7..074ecafddf3810 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -218,7 +218,7 @@ export class SecurityPageObject extends FtrService { } if (expectedResult === 'chrome') { - await this.find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); + await this.find.byCssSelector('[data-test-subj="userMenuAvatar"]', 20000); this.log.debug(`Finished login process currentUrl = ${await this.browser.getCurrentUrl()}`); } diff --git a/x-pack/test/functional/services/monitoring/cluster_list.js b/x-pack/test/functional/services/monitoring/cluster_list.js index f63e7b6cd125ec..bcf0e18ef4dd7c 100644 --- a/x-pack/test/functional/services/monitoring/cluster_list.js +++ b/x-pack/test/functional/services/monitoring/cluster_list.js @@ -15,7 +15,7 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_CLUSTER_ROW_PREFIX = `${SUBJ_TABLE_CONTAINER} > clusterRow_`; - const ALERTS_MODAL_BUTTON = 'alerts-modal-button'; + const ALERTS_MODAL_BUTTON = 'alerts-modal-remind-later-button'; return new (class ClusterList { async assertDefaults() { diff --git a/x-pack/test/functional/services/monitoring/cluster_overview.js b/x-pack/test/functional/services/monitoring/cluster_overview.js index 5128cbffd34cff..5874606056db9a 100644 --- a/x-pack/test/functional/services/monitoring/cluster_overview.js +++ b/x-pack/test/functional/services/monitoring/cluster_overview.js @@ -76,7 +76,7 @@ export function MonitoringClusterOverviewProvider({ getService }) { } closeAlertsModal() { - return testSubjects.click('alerts-modal-button'); + return testSubjects.click('alerts-modal-remind-later-button'); } getEsStatus() { diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 7c4a45fb601ea8..9e3ffcdfd8095d 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -37,6 +37,11 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { .find('.euiTableCellContent') .text() .trim(), + type: $tr + .findTestSubject('transformListColumnType') + .find('.euiTableCellContent') + .text() + .trim(), status: $tr .findTestSubject('transformListColumnStatus') .find('.euiTableCellContent') @@ -190,38 +195,54 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } - public async assertTransformExpandedRow() { - await testSubjects.click('transformListRowDetailsToggle'); + public async ensureDetailsOpen() { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('transformExpandedRowTabbedContent'))) { + await testSubjects.click('transformListRowDetailsToggle'); + await testSubjects.existOrFail('transformExpandedRowTabbedContent', { timeout: 1000 }); + } + }); + } - // The expanded row should show the details tab content by default - await testSubjects.existOrFail('transformDetailsTab'); - await testSubjects.existOrFail('~transformDetailsTabContent'); + public async ensureDetailsClosed() { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('transformExpandedRowTabbedContent')) { + await testSubjects.click('transformListRowDetailsToggle'); + await testSubjects.missingOrFail('transformExpandedRowTabbedContent', { timeout: 1000 }); + } + }); + } - // Walk through the rest of the tabs and check if the corresponding content shows up - await testSubjects.existOrFail('transformJsonTab'); - await testSubjects.click('transformJsonTab'); - await testSubjects.existOrFail('~transformJsonTabContent'); + public async switchToExpandedRowTab(tabSubject: string, contentSubject: string) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.click(tabSubject); + await testSubjects.existOrFail(contentSubject, { timeout: 1000 }); + }); + } - await testSubjects.existOrFail('transformMessagesTab'); - await testSubjects.click('transformMessagesTab'); - await testSubjects.existOrFail('~transformMessagesTabContent'); + public async assertTransformExpandedRow() { + await this.ensureDetailsOpen(); + await retry.tryForTime(30 * 1000, async () => { + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab', { timeout: 1000 }); + await testSubjects.existOrFail('~transformDetailsTabContent', { timeout: 1000 }); + }); - await testSubjects.existOrFail('transformPreviewTab'); - await testSubjects.click('transformPreviewTab'); - await testSubjects.existOrFail('~transformPivotPreview'); + // Walk through the rest of the tabs and check if the corresponding content shows up + await this.switchToExpandedRowTab('transformJsonTab', '~transformJsonTabContent'); + await this.switchToExpandedRowTab('transformMessagesTab', '~transformMessagesTabContent'); + await this.switchToExpandedRowTab('transformPreviewTab', '~transformPivotPreview'); } public async assertTransformExpandedRowMessages(expectedText: string) { - await testSubjects.click('transformListRowDetailsToggle'); + await this.ensureDetailsOpen(); // The expanded row should show the details tab content by default await testSubjects.existOrFail('transformDetailsTab'); await testSubjects.existOrFail('~transformDetailsTabContent'); // Click on the messages tab and assert the messages - await testSubjects.existOrFail('transformMessagesTab'); - await testSubjects.click('transformMessagesTab'); - await testSubjects.existOrFail('~transformMessagesTabContent'); + await this.switchToExpandedRowTab('transformMessagesTab', '~transformMessagesTabContent'); await retry.tryForTime(30 * 1000, async () => { const actualText = await testSubjects.getVisibleText('~transformMessagesTabContent'); expect(actualText.toLowerCase()).to.contain( diff --git a/yarn.lock b/yarn.lock index f0e4fc1df484c8..1259728241fbee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@33.0.0": - version "33.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.0.0.tgz#45428b792300e363ecd3454465be49d42788a1fd" - integrity sha512-HUt0oBaO/PSRZcYHmdLL1lzFWMBtP/9umGIAQgW785qv+y/Hv2cjjfYNckGfnr5RUDtx0KCc3v2UBkRq6n93EA== +"@elastic/charts@33.1.0": + version "33.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.1.0.tgz#3d32a0cf2d07a8df381a8a218104f0f554d392f0" + integrity sha512-m/Qvs2xixzkYa7LeNCKajHjfRIkT2ZTlEQ+Sw52eRt0enCXjuS8Pp6PvLz0a9Ye37aLCZhEJMAS1EGJxHsX/pQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0"