diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 000943bf62d8..95c11b05a978 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,6 +158,7 @@ # Maps #CC# /x-pack/plugins/maps/ @elastic/kibana-gis +/x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis @@ -165,7 +166,9 @@ /x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis /x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +/src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis +/x-pack/plugins/file_upload @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis /packages/kbn-mapbox-gl @elastic/kibana-gis diff --git a/.gitignore b/.gitignore index c0588d850062..4f77a6e450c4 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,9 @@ report.asciidoc # TS incremental build cache *.tsbuildinfo +# Automatically generated and user-modifiable +/tsconfig.refs.json + # Yarn local mirror content .yarn-local-mirror diff --git a/BUILD.bazel b/BUILD.bazel index 1f6e3030e5d0..e838d312d876 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -3,7 +3,9 @@ exports_files( [ "tsconfig.base.json", + "tsconfig.bazel.json", "tsconfig.browser.json", + "tsconfig.browser_bazel.json", "tsconfig.json", "package.json" ], diff --git a/STYLEGUIDE.mdx b/STYLEGUIDE.mdx index afe00476640b..95f29c674da9 100644 --- a/STYLEGUIDE.mdx +++ b/STYLEGUIDE.mdx @@ -35,7 +35,7 @@ remove it, don't simply comment it out. We are gradually moving the Kibana code base over to Prettier. All TypeScript code and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code. You -can run `node script/eslint --fix` to fix linting issues and apply Prettier formatting. +can run `node scripts/eslint --fix` to fix linting issues and apply Prettier formatting. We recommend you to enable running ESLint via your IDE. Whenever possible we are trying to use Prettier and linting over written style guide rules. diff --git a/api_docs/charts.json b/api_docs/charts.json index 177a63556d59..7081f410ee8a 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -2539,7 +2539,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/charts/common/palette.ts", @@ -2597,7 +2597,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/charts/common/palette.ts", diff --git a/api_docs/dashboard.json b/api_docs/dashboard.json index 656364b835af..cf504e4452a1 100644 --- a/api_docs/dashboard.json +++ b/api_docs/dashboard.json @@ -1137,8 +1137,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/dashboard/public/locator.ts", @@ -1204,8 +1204,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], @@ -1334,8 +1334,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], diff --git a/api_docs/data.json b/api_docs/data.json index e4f94168f50b..0456a1ed0441 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -241,7 +241,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -608,7 +608,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -629,7 +629,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -1273,7 +1273,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -1512,7 +1512,7 @@ "text": "AggConfig" }, ">(params: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -1543,7 +1543,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -2274,7 +2274,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -2303,7 +2303,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -8188,7 +8188,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8270,7 +8270,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8320,7 +8320,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8434,7 +8434,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8516,7 +8516,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8598,7 +8598,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8648,7 +8648,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8762,7 +8762,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8844,7 +8844,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8958,7 +8958,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9012,7 +9012,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9062,7 +9062,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9120,7 +9120,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9178,7 +9178,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9236,7 +9236,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9294,7 +9294,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9352,7 +9352,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9402,7 +9402,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9452,7 +9452,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9506,7 +9506,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9560,7 +9560,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9610,7 +9610,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9660,7 +9660,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9710,7 +9710,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9760,7 +9760,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9810,7 +9810,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9860,7 +9860,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9914,7 +9914,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9964,7 +9964,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10014,7 +10014,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10068,7 +10068,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10118,7 +10118,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10168,7 +10168,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10218,7 +10218,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -14889,7 +14889,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -15020,7 +15020,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -15131,7 +15131,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -15170,7 +15170,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esdsl.ts", @@ -17656,7 +17656,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", @@ -24304,7 +24304,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -24333,7 +24333,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -28751,7 +28751,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28833,7 +28833,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28883,7 +28883,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28997,7 +28997,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29079,7 +29079,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29161,7 +29161,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29211,7 +29211,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29325,7 +29325,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29407,7 +29407,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29521,7 +29521,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29575,7 +29575,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29625,7 +29625,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29683,7 +29683,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29741,7 +29741,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29799,7 +29799,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29857,7 +29857,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29915,7 +29915,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29965,7 +29965,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30015,7 +30015,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30069,7 +30069,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30123,7 +30123,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30173,7 +30173,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30223,7 +30223,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30273,7 +30273,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30323,7 +30323,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30373,7 +30373,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30423,7 +30423,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30477,7 +30477,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30527,7 +30527,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30577,7 +30577,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30631,7 +30631,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30681,7 +30681,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30731,7 +30731,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30781,7 +30781,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -32535,7 +32535,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -32631,7 +32631,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -34951,7 +34951,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index 3badbb04ce52..3ca81e7ff4b8 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -8503,7 +8503,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", diff --git a/api_docs/data_query.json b/api_docs/data_query.json index 3ed8cc6ab9f0..af30ed33b030 100644 --- a/api_docs/data_query.json +++ b/api_docs/data_query.json @@ -30,8 +30,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -492,8 +492,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", collector: unknown) => {}" ], diff --git a/api_docs/data_search.json b/api_docs/data_search.json index d6006dc315db..e035cee56c6e 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -918,7 +918,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -4174,7 +4174,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -4541,7 +4541,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -4562,7 +4562,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -5206,7 +5206,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -5445,7 +5445,7 @@ "text": "AggConfig" }, ">(params: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -5476,7 +5476,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -6207,7 +6207,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -6236,7 +6236,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -11247,7 +11247,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/data/common/search/expressions/utils/function_wrapper.ts", @@ -14922,7 +14922,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15004,7 +15004,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15054,7 +15054,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15168,7 +15168,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15250,7 +15250,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15332,7 +15332,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15382,7 +15382,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15496,7 +15496,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15578,7 +15578,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15692,7 +15692,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15746,7 +15746,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15796,7 +15796,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15854,7 +15854,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15912,7 +15912,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15970,7 +15970,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16028,7 +16028,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16086,7 +16086,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16136,7 +16136,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16186,7 +16186,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16240,7 +16240,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16294,7 +16294,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16344,7 +16344,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16394,7 +16394,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16444,7 +16444,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16494,7 +16494,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16544,7 +16544,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16594,7 +16594,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16648,7 +16648,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16698,7 +16698,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16748,7 +16748,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16802,7 +16802,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16852,7 +16852,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16902,7 +16902,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16952,7 +16952,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -17102,7 +17102,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_avg.ts", @@ -17117,7 +17117,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_avg.ts", @@ -17156,7 +17156,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_max.ts", @@ -17171,7 +17171,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_max.ts", @@ -17210,7 +17210,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_min.ts", @@ -17225,7 +17225,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_min.ts", @@ -17264,7 +17264,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_sum.ts", @@ -17279,7 +17279,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_sum.ts", @@ -17365,7 +17365,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts", @@ -17702,7 +17702,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/derivative.ts", @@ -17843,7 +17843,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", @@ -17858,7 +17858,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", @@ -18557,7 +18557,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/moving_avg.ts", @@ -18770,7 +18770,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/serial_diff.ts", @@ -19027,7 +19027,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/buckets/terms.ts", @@ -23574,7 +23574,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -24027,7 +24027,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -24216,7 +24216,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_configs.ts", @@ -24356,7 +24356,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -24395,7 +24395,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esdsl.ts", @@ -24575,7 +24575,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/cidr.ts", @@ -24638,7 +24638,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/date_range.ts", @@ -24687,7 +24687,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/exists_filter.ts", @@ -24750,7 +24750,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/extended_bounds.ts", @@ -24805,7 +24805,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/field.ts", @@ -24860,7 +24860,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/geo_bounding_box.ts", @@ -24915,7 +24915,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/geo_point.ts", @@ -24978,7 +24978,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/ip_range.ts", @@ -25149,7 +25149,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", @@ -25212,7 +25212,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/timerange.ts", @@ -25261,7 +25261,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/kql.ts", @@ -25310,7 +25310,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/lucene.ts", @@ -25373,7 +25373,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/numerical_range.ts", @@ -25422,7 +25422,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", @@ -25477,7 +25477,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/query_filter.ts", @@ -25524,7 +25524,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/range.ts", @@ -25573,7 +25573,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/range_filter.ts", @@ -30132,7 +30132,7 @@ "text": "IBucketAggConfig" }, ", state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => ", { "pluginId": "data", diff --git a/api_docs/discover.json b/api_docs/discover.json index ec94f42df2a0..61d2f39d0c87 100644 --- a/api_docs/discover.json +++ b/api_docs/discover.json @@ -81,8 +81,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/discover/public/locator.ts", @@ -163,8 +163,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], @@ -278,8 +278,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], diff --git a/api_docs/embeddable.json b/api_docs/embeddable.json index 44c5ecbe0c06..7e5ef7bbdcc1 100644 --- a/api_docs/embeddable.json +++ b/api_docs/embeddable.json @@ -8095,8 +8095,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; }" ], @@ -8511,8 +8511,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => void" ], @@ -8539,8 +8539,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -9194,8 +9194,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => void" ], @@ -9222,8 +9222,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -9965,8 +9965,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; }" ], @@ -10037,16 +10037,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", version: string) => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/embeddable/common/lib/migrate.ts", diff --git a/api_docs/expression_repeat_image.json b/api_docs/expression_repeat_image.json index 4917dc1f6220..e2039e6e2ac4 100644 --- a/api_docs/expression_repeat_image.json +++ b/api_docs/expression_repeat_image.json @@ -350,7 +350,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_repeat_image/common/types/expression_functions.ts", diff --git a/api_docs/expression_shape.json b/api_docs/expression_shape.json index 607c10fb3824..94a969388a61 100644 --- a/api_docs/expression_shape.json +++ b/api_docs/expression_shape.json @@ -891,7 +891,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_shape/common/types/expression_functions.ts", @@ -1405,7 +1405,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_shape/common/types/expression_functions.ts", diff --git a/api_docs/expressions.json b/api_docs/expressions.json index 6185466a02cc..0abef59f1dee 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -76,8 +76,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -116,8 +116,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -157,7 +157,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -203,8 +203,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -350,8 +350,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -840,8 +840,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -1469,8 +1469,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -1851,8 +1851,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -1886,8 +1886,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -2301,16 +2301,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -4000,8 +4000,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -4616,8 +4616,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -4651,8 +4651,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -7709,7 +7709,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7755,7 +7755,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7793,7 +7793,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7831,7 +7831,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7869,7 +7869,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7931,7 +7931,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7993,7 +7993,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8055,7 +8055,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8117,7 +8117,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8614,8 +8614,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -9105,7 +9105,7 @@ "label": "searchContext", "description": [], "signature": [ - "SerializableState", + "Serializable", " | undefined" ], "path": "src/plugins/expressions/public/types/index.ts", @@ -10235,7 +10235,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -10638,8 +10638,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -10786,8 +10786,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -11195,8 +11195,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -11550,8 +11550,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -11590,8 +11590,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -11631,7 +11631,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -11677,8 +11677,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -11824,8 +11824,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -12745,8 +12745,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -13127,8 +13127,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -13162,8 +13162,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -13577,16 +13577,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -17443,7 +17443,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17489,7 +17489,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17527,7 +17527,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17565,7 +17565,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17603,7 +17603,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17665,7 +17665,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17727,7 +17727,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17789,7 +17789,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17851,7 +17851,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -18962,7 +18962,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -19314,8 +19314,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -19462,8 +19462,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -19839,8 +19839,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -19955,8 +19955,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -19995,8 +19995,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -20036,7 +20036,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -20082,8 +20082,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -20229,8 +20229,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -20719,8 +20719,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -21348,8 +21348,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -21730,8 +21730,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -21765,8 +21765,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -22180,16 +22180,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -23248,8 +23248,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -23864,8 +23864,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -23899,8 +23899,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -25367,8 +25367,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -25533,7 +25533,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/util/test_utils.ts", @@ -25923,8 +25923,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -26796,7 +26796,7 @@ "\nany extra parameters for the source that produced this column" ], "signature": [ - "SerializableState", + "Serializable", " | undefined" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", @@ -28555,8 +28555,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined" ], @@ -29009,7 +29009,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29055,7 +29055,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29093,7 +29093,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29131,7 +29131,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29169,7 +29169,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29231,7 +29231,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29293,7 +29293,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29355,7 +29355,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29417,7 +29417,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29882,8 +29882,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -31260,7 +31260,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -31570,8 +31570,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | undefined; rawError?: any; duration: number | undefined; }" ], @@ -31640,7 +31640,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/clog.ts", @@ -31703,7 +31703,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts", @@ -31766,7 +31766,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/derivative.ts", @@ -31813,7 +31813,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/font.ts", @@ -31876,7 +31876,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/moving_average.ts", @@ -31939,7 +31939,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/overall_metric.ts", @@ -31978,7 +31978,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -32033,7 +32033,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/ui_setting.ts", @@ -32072,7 +32072,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -32111,7 +32111,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", @@ -32271,8 +32271,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -32380,8 +32380,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -32550,8 +32550,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -34894,8 +34894,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>) => ", { @@ -34926,8 +34926,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>, \"error\" | \"info\">; }>" ], @@ -34962,8 +34962,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -36961,7 +36961,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => ", { "pluginId": "expressions", @@ -37038,7 +37038,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/math_column.ts", @@ -39686,7 +39686,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -39744,7 +39744,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -40166,7 +40166,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -40224,7 +40224,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -40429,7 +40429,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => unknown" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", @@ -40487,7 +40487,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", diff --git a/api_docs/index_lifecycle_management.json b/api_docs/index_lifecycle_management.json index 744bc3f12f3c..d1d99aa17cff 100644 --- a/api_docs/index_lifecycle_management.json +++ b/api_docs/index_lifecycle_management.json @@ -24,8 +24,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "x-pack/plugins/index_lifecycle_management/public/locator.ts", diff --git a/api_docs/kibana_utils.json b/api_docs/kibana_utils.json index fa59e3171212..2bd4e7f310b6 100644 --- a/api_docs/kibana_utils.json +++ b/api_docs/kibana_utils.json @@ -9150,8 +9150,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => S" ], @@ -9198,8 +9198,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -10007,8 +10007,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => P) | undefined" ], @@ -10037,8 +10037,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -10964,16 +10964,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", version: string) => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/kibana_utils/common/persistable_state/types.ts", @@ -11216,16 +11216,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableValue", - "text": "SerializableValue" + "section": "def-common.SerializableRecord", + "text": "SerializableRecord" }, "[] | null | undefined" ], @@ -11235,10 +11235,10 @@ }, { "parentPluginId": "kibanaUtils", - "id": "def-common.SerializableState", + "id": "def-common.Serializable", "type": "Type", "tags": [], - "label": "SerializableState", + "label": "Serializable", "description": [ "\nSerializable state is something is a POJO JavaScript object that can be\nserialized to a JSON string." ], @@ -11259,10 +11259,10 @@ }, { "parentPluginId": "kibanaUtils", - "id": "def-common.SerializableValue", + "id": "def-common.SerializableRecord", "type": "Type", "tags": [], - "label": "SerializableValue", + "label": "SerializableRecord", "description": [], "signature": [ "string | number | boolean | ", @@ -11270,8 +11270,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | null | undefined" ], diff --git a/api_docs/lens.json b/api_docs/lens.json index 34408d962cea..0e4b2aee2ac2 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -2742,8 +2742,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { diff --git a/api_docs/presentation_util.json b/api_docs/presentation_util.json index d062fa63965e..e14e04794e02 100644 --- a/api_docs/presentation_util.json +++ b/api_docs/presentation_util.json @@ -598,7 +598,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts", diff --git a/api_docs/share.json b/api_docs/share.json index d9b0ba64d94c..826348efb8eb 100644 --- a/api_docs/share.json +++ b/api_docs/share.json @@ -437,8 +437,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">(locator: ", { @@ -1884,8 +1884,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">(locator: ", { diff --git a/api_docs/ui_actions_enhanced.json b/api_docs/ui_actions_enhanced.json index 697231e9f793..0847694c7302 100644 --- a/api_docs/ui_actions_enhanced.json +++ b/api_docs/ui_actions_enhanced.json @@ -1566,8 +1566,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">, triggers: string[]) => Promise" ], @@ -1596,8 +1596,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -1647,8 +1647,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">, triggers: string[]) => Promise" ], @@ -1693,8 +1693,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -1880,8 +1880,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }[]>" ], @@ -3132,8 +3132,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", object, ", { @@ -3366,8 +3366,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], @@ -3935,8 +3935,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], @@ -4124,8 +4124,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], diff --git a/api_docs/visualizations.json b/api_docs/visualizations.json index c779b4695fde..655890dd601f 100644 --- a/api_docs/visualizations.json +++ b/api_docs/visualizations.json @@ -1256,7 +1256,7 @@ "text": "SerializedFieldFormat" }, "> | undefined; source?: string | undefined; sourceParams?: ", - "SerializableState", + "Serializable", " | undefined; }; id: string; name: string; }[]; type: \"datatable\"; rows: Record[]; }" ], "path": "src/plugins/visualizations/common/prepare_log_table.ts", @@ -1651,7 +1651,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -2075,7 +2075,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -4881,7 +4881,7 @@ "text": "SerializedFieldFormat" }, "> | undefined; source?: string | undefined; sourceParams?: ", - "SerializableState", + "Serializable", " | undefined; }; id: string; name: string; }[]; type: \"datatable\"; rows: Record[]; }" ], "path": "src/plugins/visualizations/common/prepare_log_table.ts", @@ -4981,7 +4981,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/visualizations/common/expression_functions/range.ts", @@ -5047,7 +5047,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/visualizations/common/expression_functions/vis_dimension.ts", @@ -5120,7 +5120,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", diff --git a/dev_docs/best_practices.mdx b/dev_docs/best_practices.mdx index d87c6eb61899..0bc86da6998d 100644 --- a/dev_docs/best_practices.mdx +++ b/dev_docs/best_practices.mdx @@ -171,7 +171,7 @@ Kibana is translated into other languages. Use our i18n utilities to ensure your ## Styleguide -We use es-lint rules when possible, but please review our [styleguide](https://github.com/elastic/kibana/blob/master/STYLEGUIDE.md), which includes recommendations that can't be linted on. +We use es-lint rules when possible, but please review our [styleguide](https://github.com/elastic/kibana/blob/master/STYLEGUIDE.mdx), which includes recommendations that can't be linted on. Es-lint overrides on a per-plugin level are discouraged. diff --git a/dev_docs/key_concepts/saved_objects.mdx b/dev_docs/key_concepts/saved_objects.mdx index d89342765c8f..bef92bf02869 100644 --- a/dev_docs/key_concepts/saved_objects.mdx +++ b/dev_docs/key_concepts/saved_objects.mdx @@ -72,3 +72,17 @@ Sometimes Saved Objects end up persisted inside another Saved Object. We call th issues with edits propagating - since an entity can only exist in a single place. Note that from the end user stand point, we don’t use these terms “by reference” and “by value”. +## Sharing Saved Objects + +Starting in Kibana 7.12, saved objects can be shared to multiple spaces. The "space behavior" is determined for each object type depending +on how it is registered. + +If you are adding a **new** object type, when you register it: + +1. Use `namespaceType: 'multiple-isolated'` to make these objects exist in exactly one space +2. Use `namespaceType: 'multiple'` to make these objects exist in one *or more* spaces +3. Use `namespaceType: 'agnostic'` if you want these objects to always exist in all spaces + +If you have an **existing** "legacy" object type that is not shareable (using `namespaceType: 'single'`), see the [legacy developer guide +for Sharing Saved Objects](https://www.elastic.co/guide/en/kibana/master/sharing-saved-objects.html) for details on steps you need to take +to make sure this is converted to `namespaceType: 'multiple-isolated'` or `namespaceType: 'multiple'` in the 8.0 release. diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index bd7d231218af..35efbb97a0a0 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -19,7 +19,7 @@ import { SavedObjectsType } from 'src/core/server'; export const dashboardVisualization: SavedObjectsType = { name: 'dashboard_visualization', [1] hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', [2] mappings: { dynamic: false, properties: { @@ -41,6 +41,10 @@ export const dashboardVisualization: SavedObjectsType = { [1] Since the name of a Saved Object type forms part of the url path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written as snake case. +[2] This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means +that objects of this type can only exist in a single space. See + for more information. + **src/plugins/my_plugin/server/saved_objects/index.ts** ```ts @@ -122,7 +126,7 @@ Since Elasticsearch has a default limit of 1000 fields per index, plugins should fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the .kibana index. - ## References +## References Declare by adding an id, type and name to the `references` array. @@ -155,12 +159,14 @@ identify this reference. This guarantees that the id the reference points to alw visualization id was directly stored in `dashboard.panels[0].visualization` there is a risk that this id gets updated without updating the reference in the references array. -## Writing migrations +## Migrations Saved Objects support schema changes between Kibana versions, which we call migrations. Migrations are - applied when a Kibana installation is upgraded from one version to the next, when exports are imported via + applied when a Kibana installation is upgraded from one version to a newer version, when exports are imported via the Saved Objects Management UI, or when a new object is created via the HTTP API. +### Writing migrations + Each Saved Object type may define migrations for its schema. Migrations are specified by the Kibana version number, receive an input document, and must return the fully migrated document to be persisted to Elasticsearch. @@ -241,10 +247,11 @@ export const dashboardVisualization: SavedObjectsType = { in which this migration was released. So if you are creating a migration which will be part of the v7.10.0 release, but will also be backported and released as v7.9.3, the migration version should be: 7.9.3. - Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. - Having said that, if a - document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to - fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. +Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. +Having said that, if a document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to +fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered, +the error should be verbose and informative so that the corrupt document can be corrected, if possible. + +### Testing Migrations -It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch -conditions in a migration function and the high impact of a bug in this code, there’s really no reason not to aim for 100% test code coverage. +Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the . diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index 96e13555a36a..55b662421cbd 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -569,7 +569,7 @@ describe('renderApp', () => { }); ``` -### SavedObjects +### SavedObjectsClient #### Unit Tests @@ -794,6 +794,192 @@ Kibana and esArchiver to load fixture data into Elasticsearch. _todo: fully worked out example_ +### Saved Objects migrations + +_Also see ._ + +It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input +documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a +bug in this code, there’s really no reason not to aim for 100% test code coverage. + +It's recommend that you primarily leverage unit testing with Jest for testing your migration function. Unit tests will +be a much more effective approach to testing all the different shapes of input data and edge cases that your migration +may need to handle. With more complex migrations that interact with several components or may behave different depending +on registry contents (such as Embeddable migrations), we recommend that you use the Jest Integration suite which allows +you to create a full instance Kibana and all plugins in memory and leverage the import API to test migrating documents. + +#### Throwing exceptions +Keep in mind that any exception thrown by your migration function will cause Kibana to fail to upgrade. This should almost +never happen for our end users and we should be exhaustive in our testing to be sure to catch as many edge cases that we +could possibly handle. This entails ensuring that the migration is written defensively; we should try to avoid every bug +possible in our implementation. + +In general, exceptions should only be thrown when the input data is corrupted and doesn't match the expected schema. In +such cases, it's important that an informative error message is included in the exception and we do not rely on implicit +runtime exceptions such as "null pointer exceptions" like `TypeError: Cannot read property 'foo' of undefined`. + +#### Unit testing + +Unit testing migration functions is typically pretty straight forward and comparable to other types of Jest testing. In +general, you should focus this tier of testing on validating output and testing input edge cases. One focus of this tier +should be trying to find edge cases that throw exceptions the migration shouldn't. As you can see in this simple +example, the coverage here is very exhaustive and verbose, which is intentional. + +```ts +import { migrateCaseFromV7_9_0ToV7_10_0 } from './case_migrations'; + +const validInput_7_9_0 = { + id: '1', + type: 'case', + attributes: { + connector_id: '1234'; + } +} + +describe('Case migrations v7.7.0 -> v7.8.0', () => { + it('transforms the connector field', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0(validInput_7_9_0)).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: '1234', // verify id was moved into subobject + name: 'none', // verify new default field was added + } + } + }); + }); + + it('handles empty string', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: '' + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles null', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: null + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles undefined', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // Even though undefined isn't a valid JSON or Elasticsearch value, we should test it anyways since there + // could be some JavaScript layer that casts the field to `undefined` for some reason. + connector_id: undefined + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // also test without the field present at all + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); +}); +``` + +#### Integration testing +With more complicated migrations, the behavior of the migration may be dependent on values from other plugins which may +be difficult or even impossible to test with unit tests. You need to actually bootstrap Kibana, load the plugins, and +then test the full end-to-end migration. This type of set up will also test ingesting your documents into Elasticsearch +against the mappings defined by your Saved Object type. + +This can be achieved using the `jest_integration` suite and the `kbnTestServer` utility for starting an in-memory +instance of Kibana. You can then leverage the import API to test migrations. This API applies the same migrations to +imported documents as are applied at Kibana startup and is much easier to work with for testing. + +```ts +// You may need to adjust these paths depending on where your test file is located. +// The absolute path is src/core/test_helpers/so_migrations +import { createTestHarness, SavedObjectTestHarness } from '../../../../src/core/test_helpers/so_migrations'; + +describe('my plugin migrations', () => { + let testHarness: SavedObjectTestHarness; + + beforeAll(async () => { + testHarness = createTestHarness(); + await testHarness.start(); + }); + + afterAll(async () => { + await testHarness.stop(); + }); + + it('successfully migrates valid case documents', async () => { + expect( + await testHarness.migrate([ + { type: 'case', id: '1', attributes: { connector_id: '1234' }, references: [] }, + { type: 'case', id: '2', attributes: { connector_id: '' }, references: [] }, + { type: 'case', id: '3', attributes: { connector_id: null }, references: [] }, + ]) + ).toEqual([ + expect.objectContaining( + { type: 'case', id: '1', attributes: { connector: { id: '1234', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '2', attributes: { connector: { id: 'none', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '3', attributes: { connector: { id: 'none', name: 'none' } } }), + ]) + }) +}) +``` + +There are some caveats about using the import/export API for testing migrations: +- You cannot test the startup behavior of Kibana this way. This should not have any effect on type migrations but does + mean that this method cannot be used for testing the migration algorithm itself. +- While not yet supported, if support is added for migrations that affect multiple types, it's possible that the + behavior during import may vary slightly from the upgrade behavior. + ### Elasticsearch _How to test ES clients_ diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index b2ce650a531d..96a7e57ef3e4 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -18,8 +18,6 @@ This section summarizes the changes in each release. [[release-notes-8.0.0-alpha1]] == {kib} 8.0.0-alpha1 -coming[8.0.0] - The following changes are released for the first time in {kib} 8.0.0-alpha1. Review the changes, then use the <> to complete the upgrade. [float] diff --git a/docs/api/saved-objects/resolve.asciidoc b/docs/api/saved-objects/resolve.asciidoc index f2bf31bc5d9e..abfad6a0a815 100644 --- a/docs/api/saved-objects/resolve.asciidoc +++ b/docs/api/saved-objects/resolve.asciidoc @@ -70,6 +70,8 @@ The `outcome` field may be any of the following: * `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID. * `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID. +If the outcome is `"aliasMatch"` or `"conflict"`, the response will also include an `alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID. + Retrieve a dashboard object in the `testspace` by ID: [source,sh] @@ -125,6 +127,7 @@ The API returns the following: "dashboard": "7.0.0" } }, - "outcome": "conflict" + "outcome": "conflict", + "alias_target_id": "05becb88-e214-439a-a2ac-15fc783b5d01" } -------------------------------------------------- diff --git a/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png b/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png new file mode 100644 index 000000000000..bc829059988d Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-1.png b/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-1.png new file mode 100644 index 000000000000..5c41f19392d7 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-1.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png b/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png new file mode 100644 index 000000000000..50160d7a7b0f Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-1.png b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-1.png new file mode 100644 index 000000000000..34733de65859 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-1.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png new file mode 100644 index 000000000000..7c9dcb5fb9b0 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png new file mode 100644 index 000000000000..2d4c0c4399d8 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-overview.png b/docs/developer/advanced/images/sharing-saved-objects-overview.png new file mode 100644 index 000000000000..5c08eaa286ae Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-overview.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-q2.png b/docs/developer/advanced/images/sharing-saved-objects-q2.png new file mode 100644 index 000000000000..f453f90c160a Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-q2.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-1.png b/docs/developer/advanced/images/sharing-saved-objects-step-1.png new file mode 100644 index 000000000000..56ef017354d3 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-step-1.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-3.png b/docs/developer/advanced/images/sharing-saved-objects-step-3.png new file mode 100644 index 000000000000..92dd7ebfef88 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-step-3.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-4.png b/docs/developer/advanced/images/sharing-saved-objects-step-4.png new file mode 100644 index 000000000000..73c5848521a1 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-step-4.png differ diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-5.png b/docs/developer/advanced/images/sharing-saved-objects-step-5.png new file mode 100644 index 000000000000..07e74e44d4e6 Binary files /dev/null and b/docs/developer/advanced/images/sharing-saved-objects-step-5.png differ diff --git a/docs/developer/advanced/index.asciidoc b/docs/developer/advanced/index.asciidoc index ea1cc810e8ff..289b88cddd7a 100644 --- a/docs/developer/advanced/index.asciidoc +++ b/docs/developer/advanced/index.asciidoc @@ -5,6 +5,7 @@ * <> * <> * <> +* <> include::development-es-snapshots.asciidoc[leveloffset=+1] @@ -12,4 +13,6 @@ include::running-elasticsearch.asciidoc[leveloffset=+1] include::development-basepath.asciidoc[leveloffset=+1] -include::upgrading-nodejs.asciidoc[leveloffset=+1] \ No newline at end of file +include::upgrading-nodejs.asciidoc[leveloffset=+1] + +include::sharing-saved-objects.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index 324d2af2ed3a..36f9ee420d41 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -76,7 +76,6 @@ If many other users will be interacting with your remote cluster, you'll want to [source,bash] ---- kibana.index: '.{YourGitHubHandle}-kibana' -xpack.task_manager.index: '.{YourGitHubHandle}-task-manager-kibana' ---- ==== Running remote clusters diff --git a/docs/developer/advanced/sharing-saved-objects.asciidoc b/docs/developer/advanced/sharing-saved-objects.asciidoc new file mode 100644 index 000000000000..19c1b8065728 --- /dev/null +++ b/docs/developer/advanced/sharing-saved-objects.asciidoc @@ -0,0 +1,472 @@ +[[sharing-saved-objects]] +== Sharing Saved Objects + +This guide describes the Sharing Saved Objects effort, and the breaking changes that plugin developers need to be aware of for the planned +8.0 release of {kib}. + +[[sharing-saved-objects-overview]] +=== Overview + +<> (hereinafter "objects") are used to store all sorts of things in {kib}, from Dashboards to Index +Patterns to Machine Learning Jobs. The effort to make objects shareable can be summarized in a single picture: + +image::images/sharing-saved-objects-overview.png["Sharing Saved Objects overview"] + +Each plugin can register different object types to be used in {kib}. Historically, objects could be _isolated_ (existing in a single +<>) or _global_ (existing in all spaces), there was no in-between. As of the 7.12 release, {kib} now supports two +additional types of objects: + +|====================================================================================================== +| | *Where it exists* | *Object IDs* | *Registered as:* +| Global | All spaces | Globally unique | `namespaceType: 'agnostic'` +| Isolated | 1 space | Unique in each space | `namespaceType: 'single'` +| (NEW) Share-capable | 1 space | Globally unique | `namespaceType: 'multiple-isolated'` +| (NEW) Shareable | 1 or more spaces | Globally unique | `namespaceType: 'multiple'` +|====================================================================================================== + +Ideally, most types of objects in {kib} will eventually be _shareable_; however, we have also introduced +<> as a stepping stone for plugin developers to fully support +this feature. + +[[sharing-saved-objects-breaking-changes]] +=== Breaking changes + +To implement this feature, we had to make a key change to how objects are serialized into raw {es} documents. As a result, +<>, and this will cause some breaking changes to +the way that consumers (plugin developers) interact with objects. We have implemented mitigations so that *these changes will not affect +end-users _if_ consumers implement the required steps below.* + +Existing, isolated object types will need to go through a special _conversion process_ to become share-capable upon upgrading {kib} to +version 8.0. Once objects are converted, they can easily be switched to become fully shareable in any future release. This conversion will +change the IDs of any existing objects that are not in the Default space. Changing object IDs itself has several knock-on effects: + +* Nonstandard links to other objects can break - _mitigated by <>_ +* "Deep link" pages (URLs) to objects can break - _mitigated by <> and <>_ +* Encrypted objects may not be able to be decrypted - _mitigated by <>_ + +*To be perfectly clear: these effects will all be mitigated _if and only if_ you follow the steps below!* + +TIP: External plugins can also convert their objects, but <>. + +[[sharing-saved-objects-dev-flowchart]] +=== Developer Flowchart + +If you're still reading this page, you're probably developing a {kib} plugin that registers an object type, and you want to know what steps +you need to take to prepare for the 8.0 release and mitigate any breaking changes! Depending on how you are using saved objects, you may +need to take up to 5 steps, which are detailed in separate sections below. Refer to this flowchart: + +image::images/sharing-saved-objects-dev-flowchart.png["Sharing Saved Objects developer flowchart"] + +TIP: There is a proof-of-concept (POC) pull request to demonstrate these changes. It first adds a simple test plugin that allows users to +create and view notes. Then, it goes through the steps of the flowchart to convert the isolated "note" objects to become share-capable. As +you read this guide, you can https://github.com/elastic/kibana/pull/107256[follow along in the POC] to see exactly how to take these steps. + +[[sharing-saved-objects-q1]] +=== Question 1 + +> *Do these objects contain links to other objects?* + +If your objects store _any_ links to other objects (with an object type/ID), you need to take specific steps to ensure that these links +continue functioning after the 8.0 upgrade. + +[[sharing-saved-objects-step-1]] +=== Step 1 + +⚠️ This step *must* be completed no later than the 7.16 release. ⚠️ + +> *Ensure all object links use the root-level `references` field* + +If you answered "Yes" to <>, you need to make sure that your object links are _only_ stored in the root-level +`references` field. When a given object's ID is changed, this field will be updated accordingly for other objects. + +The image below shows two different examples of object links from a "case" object to an "action" object. The top shows the incorrect way to +link to another object, and the bottom shows the correct way. + +image::images/sharing-saved-objects-step-1.png["Sharing Saved Objects step 1"] + +If your objects _do not_ use the root-level `references` field, you'll need to <> +_before the 8.0 release_ to fix that. Here's a migration function for the example above: + +```ts +function migrateCaseToV716( + doc: SavedObjectUnsanitizedDoc<{ connector: { type: string; id: string } }> +): SavedObjectSanitizedDoc { + const { + connector: { type: connectorType, id: connectorId, ...otherConnectorAttrs }, + } = doc.attributes; + const { references = [] } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + connector: otherConnectorAttrs, + }, + references: [...references, { type: connectorType, id: connectorId, name: 'connector' }], + }; +} + +... + +// Use this migration function where the "case" object type is registered +migrations: { + '7.16.0': migrateCaseToV716, +}, +``` + +NOTE: Reminder, don't forget to add unit tests and integration tests! + +[[sharing-saved-objects-q2]] +=== Question 2 + +> *Are there any "deep links" to these objects?* + +A deep link is a URL to a page that shows a specific object. End-users may bookmark these URLs or schedule reports with them, so it is +critical to ensure that these URLs continue working. The image below shows an example of a deep link to a Canvas workpad object: + +image::images/sharing-saved-objects-q2.png["Sharing Saved Objects deep link example"] + +Note that some URLs may contain <>, for example, a +Dashboard _and_ a filter for an Index Pattern. + +[[sharing-saved-objects-step-2]] +=== Step 2 + +⚠️ This step will preferably be completed in the 7.16 release; it *must* be completed no later than the 8.0 release. ⚠️ + +> *Update your code to use the new SavedObjectsClient `resolve()` method instead of `get()`* + +If you answered "Yes" to <>, you need to make sure that when you use the SavedObjectsClient to fetch an object +using its ID, you use a different API to do so. The existing `get()` function will only find an object using its current ID. To make sure +your existing deep link URLs don't break, you should use the new `resolve()` function; <>. + +In a nutshell, if your deep link page had something like this before: + +```ts +const savedObject = savedObjectsClient.get(objType, objId); +``` + +You'll need to change it to this: + +```ts +const resolveResult = savedObjectsClient.resolve(objType, objId); +const savedObject = resolveResult.saved_object; +``` + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 2 of the POC]! + +The +https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md[SavedObjectsResolveResponse +interface] has three fields, summarized below: + +* `saved_object` - The saved object that was found. +* `outcome` - One of the following values: `'exactMatch' | 'aliasMatch' | 'conflict'` +* `alias_target_id` - This is defined if the outcome is `'aliasMatch'` or `'conflict'`. It means that a legacy URL alias with this ID points + to an object with a _different_ ID. + +The SavedObjectsClient is available both on the server-side and the client-side. You may be fetching the object on the server-side via a +custom HTTP route, or you may be fetching it on the client-side directly. Either way, the `outcome` and `alias_target_id` fields need to be +passed to your client-side code, and you should update your UI accordingly in the next step. + +NOTE: You don't need to use `resolve()` everywhere, <>! + +[[sharing-saved-objects-step-3]] +=== Step 3 + +⚠️ This step will preferably be completed in the 7.16 release; it *must* be completed no later than the 8.0 release. ⚠️ + +> *Update your _client-side code_ to correctly handle the three different `resolve()` outcomes* + +The Spaces plugin API exposes React components and functions that you should use to render your UI in a consistent manner for end-users. +Your UI will need to use the Core HTTP service and the Spaces plugin API to do this. + +Your page should change <>: + +image::images/sharing-saved-objects-step-3.png["Sharing Saved Objects resolve outcomes overview"] + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 3 of the POC]! + +1. Update your plugin's `kibana.json` to add a dependency on the Spaces plugin: ++ +```ts +... +"optionalPlugins": ["spaces"] +``` + +2. Update your plugin's `tsconfig.json` to add a dependency to the Space's plugin's type definitions: ++ +```ts +... +"references": [ + ... + { "path": "../spaces/tsconfig.json" }, +] +``` + +3. Update your Plugin class implementation to depend on the Core HTTP service and Spaces plugin API: ++ +```ts +interface PluginStartDeps { + spaces?: SpacesPluginStart; +} + +export class MyPlugin implements Plugin<{}, {}, {}, PluginStartDeps> { + public setup(core: CoreSetup) { + core.application.register({ + ... + async mount(appMountParams: AppMountParameters) { + const [coreStart, pluginStartDeps] = await core.getStartServices(); + const { http } = coreStart; + const { spaces: spacesApi } = pluginStartDeps; + ... + // pass `http` and `spacesApi` to your app when you render it + }, + }); + ... + } +} +``` + +4. In your deep link page, add a check for the `'aliasMatch'` outcome: ++ +```ts +if (spacesApi && resolveResult.outcome === 'aliasMatch') { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = resolveResult.alias_target_id!; // This is always defined if outcome === 'aliasMatch' + const newPath = http.basePath.prepend( + `path/to/this/page/${newObjectId}${window.location.hash}` + ); + await spacesApi.ui.redirectLegacyUrl(newPath, OBJECT_NOUN); + return; +} +``` +_Note that `OBJECT_NOUN` is optional, it just changes "object" in the toast to whatever you specify -- you may want the toast to say +"dashboard" or "index pattern" instead!_ + +5. And finally, in your deep link page, add a function that will create a callout in the case of a `'conflict'` outcome: ++ +```tsx +const getLegacyUrlConflictCallout = () => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (spacesApi && resolveResult.outcome === 'conflict') { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const currentObjectId = savedObject.id; + const otherObjectId = resolveResult.alias_target_id!; // This is always defined if outcome === 'conflict' + const otherObjectPath = http.basePath.prepend( + `path/to/this/page/${otherObjectId}${window.location.hash}` + ); + return ( + <> + {spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: OBJECT_NOUN, + currentObjectId, + otherObjectId, + otherObjectPath, + })} + + + ); + } + return null; +}; +... +return ( + + + + {/* If we have a legacy URL conflict callout to display, show it at the top of the page */} + {getLegacyUrlConflictCallout()} + +... +); +``` + +6. https://github.com/elastic/kibana/pull/107099#issuecomment-891147792[Generate staging data and test your page's behavior with the +different outcomes.] + +NOTE: Reminder, don't forget to add unit tests and functional tests! + +[[sharing-saved-objects-step-4]] +=== Step 4 + +⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ + +> *Update your _server-side code_ to convert these objects to become "share-capable"* + +After <> is complete, you can add the code to convert your objects. + +WARNING: The previous steps can be backported to the 7.x branch, but this step -- the conversion itself -- can only take place in 8.0! +You should use a separate pull request for this. + +When you register your object, you need to change the `namespaceType` and also add a `convertToMultiNamespaceTypeVersion` field. This +special field will trigger the actual conversion that will take place during the Core migration upgrade process when a user installs the +Kibana 8.0 release: + +image::images/sharing-saved-objects-step-4.png["Sharing Saved Objects conversion code"] + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 4 of the POC]! + +NOTE: Reminder, don't forget to add integration tests! + +[[sharing-saved-objects-q3]] +=== Question 3 + +> *Are these objects encrypted?* + +Saved objects can optionally be <> by using the Encrypted Saved Objects plugin. Very few +object types are encrypted, so most plugin developers will not be affected. + +[[sharing-saved-objects-step-5]] +=== Step 5 + +⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ + +> *Update your _server-side code_ to add an Encrypted Saved Object (ESO) migration for these objects* + +If you answered "Yes" to <>, you need to take additional steps to make sure that your objects can still be +decrypted after the conversion process. Encrypted saved objects use some fields as part of "additionally authenticated data" (AAD) to defend +against different types of cryptographic attacks. The object ID is part of this AAD, and so it follows that the after the object's ID is +changed, the object will not be able to be decrypted with the standard process. + +To mitigate this, you need to add a "no-op" ESO migration that will be applied immediately after the object is converted during the 8.0 +upgrade process. This will decrypt the object using its old ID and then re-encrypt it using its new ID: + +image::images/sharing-saved-objects-step-5.png["Sharing Saved Objects ESO migration"] + +NOTE: Reminder, don't forget to add unit tests and integration tests! + +[[sharing-saved-objects-step-6]] +=== Step 6 + +> *Update your code to make your objects shareable* + +_This is not required for the 8.0 release; this additional information will be added in the near future!_ + +[[sharing-saved-objects-faq]] +=== Frequently asked questions (FAQ) + +[[sharing-saved-objects-faq-share-capable-vs-shareable]] +==== 1. Why are there both "share-capable" and "shareable" object types? + +We implemented the share-capable object type as an intermediate step for consumers who currently have isolated objects, but are not yet +ready to support fully shareable objects. This is primarily because we want to make sure all object types are converted at the same time in +the 8.0 release to minimize confusion and disruption for the end-user experience. + +We realize that the conversion process and all that it entails can be a not-insignificant amount of work for some Kibana teams to prepare +for by the 8.0 release. As long as an object is made share-capable, that ensures that its ID will be globally unique, so it will be trivial +to make that object shareable later on when the time is right. + +A developer can easily flip a switch to make a share-capable object into a shareable one, since these are both serialized the same way. +However, we envision that each consumer will need to enact their own plan and make additional UI changes when making an object shareable. +For example, some users may not have access to the Saved Objects Management page, but we still want those users to be able to see what +space(s) their objects exist in and share them to other spaces. Each application should add the appropriate UI controls to handle this. + + +[[sharing-saved-objects-faq-changing-object-ids]] +==== 2. Why do object IDs need to be changed? + +This is because of how isolated objects are serialized to raw Elasticsearch documents. Each raw document ID today contains its space ID +(_namespace_) as a prefix. When objects are copied or imported to other spaces, they keep the same object ID, they just have a different +prefix when they are serialized to Elasticsearch. This has resulted in a situation where many Kibana installations have saved objects in +different spaces with the same object ID: + +image::images/sharing-saved-objects-faq-changing-object-ids-1.png["Sharing Saved Objects object ID diagram (before conversion)"] + +Once an object is converted, we need to remove this prefix. Because of limitations with our migration process, we cannot actively check if +this would result in a conflict. Therefore, we decided to pre-emptively regenerate the object ID for every object in a non-Default space to +ensure that every object ID becomes globally unique: + +image::images/sharing-saved-objects-faq-changing-object-ids-2.png["Sharing Saved Objects object ID diagram (after conversion)"] + +[[sharing-saved-objects-faq-multiple-deep-link-objects]] +==== 3. What if one page has deep links to multiple objects? + +As mentioned in <>, some URLs may contain multiple object IDs, effectively deep linking to multiple objects. +These should be handled on a case-by-case basis at the plugin owner's discretion. A good rule of thumb is: + +* The "primary" object on the page should always handle the three `resolve()` outcomes as described in <>. +* Any "secondary" objects on the page may handle the outcomes differently. If the secondary object ID is not important (for example, it just + functions as a page anchor), it may make more sense to ignore the different outcomes. If the secondary object _is_ important but it is not + directly represented in the UI, it may make more sense to throw a descriptive error when a `'conflict'` outcome is encountered. + - If the secondary object is resolved by an external service (such as the index pattern service), the service should simply make the full + outcome available to consumers. + +Ideally, if a secondary object on a deep link page resolves to an `'aliasMatch'` outcome, the consumer should redirect the user to a URL +with the new ID and display a toast message. The reason for this is that we don't want users relying on legacy URL aliases more often than +necessary. However, such handling of secondary objects is not considered critical for the 8.0 release. + +[[sharing-saved-objects-faq-legacy-url-alias]] +==== 4. What is a "legacy URL alias"? + +As depicted above, when an object is converted to become share-capable, if it exists in a non-Default space, its ID gets changed. To +preserve its old ID, we also create a special object called a _legacy URL alias_ ("alias" for short); this alias retains the target object's +old ID (_sourceId_), and it contains a pointer to the target object's new ID (_targetId_). + +Aliases are meant to be mostly invisible to end-users by design. There is no UI to manage them directly. Our vision is that aliases will be +used as a stop-gap to help us through the 8.0 upgrade process, but we will nudge users away from relying on aliases so we can eventually +deprecate and remove them. + +[[sharing-saved-objects-faq-resolve-outcomes]] +==== 5. Why are there three different resolve outcomes? + +The `resolve()` function first checks if an object with the given ID exists, and then it checks if an object has an alias with the given ID. + +1. If only the former is true, the outcome is an `'exactMatch'` -- we found the exact object we were looking for. +2. If only the latter is true, the outcome is an `'aliasMatch'` -- we found an alias with this ID, that pointed us to an object with a +different ID. +3. Finally, if _both conditions_ are true, the outcome is a `'conflict'` -- we found two objects using this ID. Instead of returning an +error in this situation, in the interest of usability, we decided to return the _most correct match_, which is the exact match. By informing +the consumer that this is a conflict, the consumer can render an appropriate UI to the end-user explaining that this might not be the object +they are actually looking for. + +*Outcome 1* + +When you resolve an object with its current ID, the outcome is an `'exactMatch'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-1.png["Sharing Saved Objects resolve outcome 1 (exactMatch)"] + +This can happen in the Default space _and_ in non-Default spaces. + +*Outcome 2* + +When you resolve an object with its old ID (the ID of its alias), the outcome is an `'aliasMatch'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-2.png["Sharing Saved Objects resolve outcome 2 (aliasMatch)"] + +This outcome can only happen in non-Default spaces. + +*Outcome 3* + +The third outcome is an edge case that is a combination of the others. If you resolve an object ID and two objects are found -- one as an +exact match, the other as an alias match -- the outcome is a `'conflict'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-3.png["Sharing Saved Objects resolve outcome 3 (conflict)"] + +We actually have controls in place to prevent this scenario from happening when you share, import, or copy +objects. However, this scenario _could_ still happen in a few different situations, if objects are created a certain way or if a user +tampers with an object's raw ES document. Since we can't 100% rule out this scenario, we must handle it gracefully, but we do expect this +will be a rare occurrence. + +It is important to note that when a `'conflict'` occurs, the object that is returned is the "most correct" match -- the one with the ID that +exactly matches. + +[[sharing-saved-objects-faq-resolve-instead-of-get]] +==== 6. Should I always use resolve instead of get? + +Reading through this guide, you may think it is safer or better to use `resolve()` everywhere instead of `get()`. Actually, we made an +explicit design decision to add a separate `resolve()` function because we want to limit the affects of and reliance upon legacy URL +aliases. To that end, we collect anonymous usage data based on how many times `resolve()` is used and the different outcomes are +encountered. That usage data is less useful is `resolve()` is used more often than necessary. + +Ultimately, `resolve()` should _only_ be used for data flows that involve a user-controlled deep link to an object. There is no reason to +change any other data flows to use `resolve()`. + +[[sharing-saved-objects-faq-external-plugins]] +==== 7. What about external plugins? + +External plugins (those not shipped with {kib}) can use this guide to convert any isolated objects to become share-capable or fully +shareable! If you are an external plugin developer, the steps are the same, but you don't need to worry about getting anything done before a +specific release. The only thing you need to know is that your plugin cannot convert your objects until the 8.0 release. diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index fa7fc4233259..a7ce86ea4635 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -45,7 +45,7 @@ import { SavedObjectsType } from 'src/core/server'; export const dashboardVisualization: SavedObjectsType = { name: 'dashboard_visualization', // <1> hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', // <2> mappings: { dynamic: false, properties: { @@ -66,6 +66,8 @@ export const dashboardVisualization: SavedObjectsType = { <1> Since the name of a Saved Object type forms part of the url path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written as snake case. +<2> This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means +that objects of this type can only exist in a single space. See <> for more information. .src/plugins/my_plugin/server/saved_objects/index.ts [source,typescript] @@ -153,6 +155,7 @@ should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the `.kibana` index. +[[saved-objects-service-writing-migrations]] ==== Writing Migrations Saved Objects support schema changes between Kibana versions, which we call diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index b048e59e6c98..04422a613475 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -45,7 +45,7 @@ guidelines] === Conventions * Become familiar with our -{kib-repo}blob/{branch}/STYLEGUIDE.md[styleguide] +{kib-repo}blob/{branch}/STYLEGUIDE.mdx[styleguide] (use Typescript!) * Write all new code on {kib-repo}blob/{branch}/src/core/README.md[the diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index f6db3fdffcb6..6058cb4945e1 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -41,7 +41,6 @@ Additionally, in order to migrate into project refs, you also need to make sure { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/docs/developer/contributing/linting.asciidoc b/docs/developer/contributing/linting.asciidoc index eb7c22c517e4..144e23c22853 100644 --- a/docs/developer/contributing/linting.asciidoc +++ b/docs/developer/contributing/linting.asciidoc @@ -2,7 +2,7 @@ == Linting A note about linting: We use http://eslint.org[eslint] to check that the -link:STYLEGUIDE.md[styleguide] is being followed. It runs in a +link:STYLEGUIDE.mdx[styleguide] is being followed. It runs in a pre-commit hook and as a part of the tests, but most contributors integrate it with their code editors for real-time feedback. diff --git a/docs/developer/contributing/pr-review.asciidoc b/docs/developer/contributing/pr-review.asciidoc index 885725795b0b..95f012d569c3 100644 --- a/docs/developer/contributing/pr-review.asciidoc +++ b/docs/developer/contributing/pr-review.asciidoc @@ -75,7 +75,7 @@ Reviewers are not simply evaluating the code itself, they are also evaluating th Having a relatively consistent codebase is an important part of us building a sustainable project. With dozens of active contributors at any given time, we rely on automation to help ensure consistency - we enforce a comprehensive set of linting rules through CI. We're also rolling out prettier to make this even more automatic. -For things that can't be easily automated, we maintain a link:{kib-repo}tree/{branch}/STYLEGUIDE.md[style guide] that authors should adhere to and reviewers should keep in mind when they review a pull request. +For things that can't be easily automated, we maintain a link:{kib-repo}tree/{branch}/STYLEGUIDE.mdx[style guide] that authors should adhere to and reviewers should keep in mind when they review a pull request. Beyond that, we're into subjective territory. Statements like "this isn't very readable" are hardly helpful since they can't be qualified, but that doesn't mean a reviewer should outright ignore code that is hard to understand due to how it is written. There isn't one definitively "best" way to write any particular code, so pursuing such shouldn't be our goal. Instead, reviewers and authors alike must accept that there are likely many different appropriate ways to accomplish the same thing with code, and so long as the contribution is utilizing one of those ways, then we're in good shape. diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md b/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md deleted file mode 100644 index 66c5f3efa2d8..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) - -## CoreStart.executionContext property - -[ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) - -Signature: - -```typescript -executionContext: ExecutionContextServiceStart; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index df1929b1f20a..6ad9adca53ef 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -20,7 +20,6 @@ export interface CoreStart | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | | [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | -| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | ExecutionContextServiceStart | [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md deleted file mode 100644 index b36f8ade848e..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) > [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) - -## ExecutionContextServiceStart.create property - -Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers. - -```js -const context = executionContext.create(...); -http.fetch('/endpoint/', { context }); - -``` - -Signature: - -```typescript -create: (context: KibanaExecutionContext) => IExecutionContextContainer; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md deleted file mode 100644 index d3eecf601ba9..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) - -## ExecutionContextServiceStart interface - - -Signature: - -```typescript -export interface ExecutionContextServiceStart -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) | (context: KibanaExecutionContext) => IExecutionContextContainer | Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers. -```js -const context = executionContext.create(...); -http.fetch('/endpoint/', { context }); - -``` - | - diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md index 6c6ce3171aae..09ab95a5135f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md @@ -7,5 +7,5 @@ Signature: ```typescript -context?: IExecutionContextContainer; +context?: KibanaExecutionContext; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md index 020a94118901..45a48372b451 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md @@ -18,7 +18,7 @@ export interface HttpFetchOptions extends HttpRequestInit | --- | --- | --- | | [asResponse](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-core-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-core-public.httpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | | [asSystemRequest](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | boolean | Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to false. | -| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | IExecutionContextContainer | | +| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | KibanaExecutionContext | | | [headers](./kibana-plugin-core-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md). | | [prependBasePath](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | | [query](./kibana-plugin-core-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md deleted file mode 100644 index 96ca86cffbc5..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) - -## IExecutionContextContainer interface - - -Signature: - -```typescript -export interface IExecutionContextContainer -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) | () => Record<string, string> | | -| [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) | () => Readonly<KibanaExecutionContext> | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md deleted file mode 100644 index 03132d24bcca..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) - -## IExecutionContextContainer.toHeader property - -Signature: - -```typescript -toHeader: () => Record; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md deleted file mode 100644 index 916148141c8f..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) - -## IExecutionContextContainer.toJSON property - -Signature: - -```typescript -toJSON: () => Readonly; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md deleted file mode 100644 index ea8c543c6789..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) - -## KibanaExecutionContext.description property - -human readable description. For example, a vis title, action name - -Signature: - -```typescript -readonly description: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md deleted file mode 100644 index d17f9cb8a7ff..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) - -## KibanaExecutionContext.id property - -unique value to identify the source - -Signature: - -```typescript -readonly id: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md index 06154c814c4e..8b758715a197 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md @@ -2,22 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) -## KibanaExecutionContext interface +## KibanaExecutionContext type +Represents a meta-information about a Kibana entity initiating a search request. Signature: ```typescript -export interface KibanaExecutionContext +export declare type KibanaExecutionContext = { + readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; + readonly url?: string; + parent?: KibanaExecutionContext; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | string | unique value to identify the source | -| [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | -| [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. | -| [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | - diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md deleted file mode 100644 index 21dde32e21ce..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) - -## KibanaExecutionContext.name property - -public name of a user-facing feature - -Signature: - -```typescript -readonly name: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md deleted file mode 100644 index 48009ebaaeaa..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) - -## KibanaExecutionContext.type property - -Kibana application initated an operation. - -Signature: - -```typescript -readonly type: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md deleted file mode 100644 index 47ad7604b473..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) - -## KibanaExecutionContext.url property - -in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url - -Signature: - -```typescript -readonly url?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index d743508e046e..e984fbb675e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -63,7 +63,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [DomainDeprecationDetails](./kibana-plugin-core-public.domaindeprecationdetails.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | -| [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) | | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-core-public.httphandler.md). | @@ -80,14 +79,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | -| [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) | | | [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. | | [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md). | | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | -| [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) | | | [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | @@ -162,6 +159,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpStart](./kibana-plugin-core-public.httpstart.md) | See [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | +| [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) | Represents a meta-information about a Kibana entity initiating a search request. | | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md similarity index 59% rename from docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md rename to docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md index 415681b2bb0d..0054f533a23d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [aliasTargetId](./kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [alias\_target\_id](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) -## ResolvedSimpleSavedObject.aliasTargetId property +## ResolvedSimpleSavedObject.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; +alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md index 43727d86296a..4936598c5879 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md @@ -16,7 +16,7 @@ export interface ResolvedSimpleSavedObject | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md) | SavedObjectsResolveResponse['aliasTargetId'] | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) | SavedObjectsResolveResponse['alias_target_id'] | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-public.resolvedsimplesavedobject.outcome.md) | SavedObjectsResolveResponse['outcome'] | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | -| [savedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md) | SimpleSavedObject<T> | The saved object that was found. | +| [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) | SimpleSavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md similarity index 55% rename from docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md rename to docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md index c05e8801768c..7d90791a26fd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [savedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) -## ResolvedSimpleSavedObject.savedObject property +## ResolvedSimpleSavedObject.saved\_object property The saved object that was found. Signature: ```typescript -savedObject: SimpleSavedObject; +saved_object: SimpleSavedObject; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md similarity index 62% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md index 02055da68688..07c55ae92236 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-public.savedobjectsresolveresponse.md) > [aliasTargetId](./kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-public.savedobjectsresolveresponse.md) > [alias\_target\_id](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) -## SavedObjectsResolveResponse.aliasTargetId property +## SavedObjectsResolveResponse.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: string; +alias_target_id?: string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md index 4345f2949d48..cdc79d8ac363 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-public.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | | [saved\_object](./kibana-plugin-core-public.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md deleted file mode 100644 index d152b9a0c5df..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [get](./kibana-plugin-core-server.executioncontextsetup.get.md) - -## ExecutionContextSetup.get() method - -Retrieves an opearation meta-data for the current async context. - -Signature: - -```typescript -get(): IExecutionContextContainer | undefined; -``` -Returns: - -`IExecutionContextContainer | undefined` - diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md index 137df77769c8..24591648ad95 100644 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md @@ -15,6 +15,5 @@ export interface ExecutionContextSetup | Method | Description | | --- | --- | -| [get()](./kibana-plugin-core-server.executioncontextsetup.get.md) | Retrieves an opearation meta-data for the current async context. | -| [set(context)](./kibana-plugin-core-server.executioncontextsetup.set.md) | Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly. | +| [withContext(context, fn)](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) | Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md deleted file mode 100644 index 4c8ba4d21b8c..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [set](./kibana-plugin-core-server.executioncontextsetup.set.md) - -## ExecutionContextSetup.set() method - -Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly. - -Signature: - -```typescript -set(context: Partial): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| context | Partial<KibanaServerExecutionContext> | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md new file mode 100644 index 000000000000..87da07120301 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [withContext](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) + +## ExecutionContextSetup.withContext() method + +Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. + +Signature: + +```typescript +withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| context | KibanaExecutionContext | undefined | | +| fn | (...args: any[]) => R | | + +Returns: + +`R` + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md index f67aa88862fe..6b643f7f72c9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md @@ -7,9 +7,9 @@ Signature: ```typescript -toJSON(): Readonly; +toJSON(): Readonly; ``` Returns: -`Readonly` +`Readonly` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md deleted file mode 100644 index 00c907b578cf..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) - -## KibanaExecutionContext.description property - -human readable description. For example, a vis title, action name - -Signature: - -```typescript -readonly description: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md deleted file mode 100644 index 4ade96691fb1..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) - -## KibanaExecutionContext.id property - -unique value to identify the source - -Signature: - -```typescript -readonly id: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md index c21eb1110ed8..db06f9b13f9f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md @@ -2,22 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) -## KibanaExecutionContext interface +## KibanaExecutionContext type +Represents a meta-information about a Kibana entity initiating a search request. Signature: ```typescript -export interface KibanaExecutionContext +export declare type KibanaExecutionContext = { + readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; + readonly url?: string; + parent?: KibanaExecutionContext; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | string | unique value to identify the source | -| [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | -| [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. | -| [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | - diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md deleted file mode 100644 index 92f58c01bcc1..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) - -## KibanaExecutionContext.name property - -public name of a user-facing feature - -Signature: - -```typescript -readonly name: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md deleted file mode 100644 index 6941bb150efd..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) - -## KibanaExecutionContext.type property - -Kibana application initated an operation. - -Signature: - -```typescript -readonly type: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md deleted file mode 100644 index dee241cd7939..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) - -## KibanaExecutionContext.url property - -in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url - -Signature: - -```typescript -readonly url?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md deleted file mode 100644 index f309e4fd0006..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) - -## KibanaServerExecutionContext interface - - -Signature: - -```typescript -export interface KibanaServerExecutionContext extends Partial -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md deleted file mode 100644 index dff3fd7f2e9f..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) > [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) - -## KibanaServerExecutionContext.requestId property - -Signature: - -```typescript -requestId: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a3f925151af6..c459a48c1ca4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -110,10 +110,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | | | [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | -| [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) | | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | -| [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) | | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 | @@ -279,6 +277,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) | | | [ISavedObjectsRepository](./kibana-plugin-core-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | [ISavedObjectTypeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) | See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) for documentation. | +| [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) | Represents a meta-information about a Kibana entity initiating a search request. | | [KibanaRequestRouteOptions](./kibana-plugin-core-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | | [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [KnownHeaders](./kibana-plugin-core-server.knownheaders.md) | Set of well-known HTTP headers. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md new file mode 100644 index 000000000000..e05f9466aa9e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createGenericNotFoundEsUnavailableError](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md) + +## SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError() method + +Signature: + +```typescript +static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 2dc78f2df3a8..67056c8a3cb5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createGenericNotFoundEsUnavailableError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md) | static | | | [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md similarity index 62% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md index 2e73d6ba2e1a..4e8bc5e787ed 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [aliasTargetId](./kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [alias\_target\_id](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) -## SavedObjectsResolveResponse.aliasTargetId property +## SavedObjectsResolveResponse.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: string; +alias_target_id?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md index 8a2504ec7adc..bbffd9902c0e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | | [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md index 7a75950f9cc6..73b415f0a0b8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md @@ -9,9 +9,9 @@ Returns a serialized field format for the field used in this agg. This can be pa Signature: ```typescript -toSerializedFieldFormat(): {} | Ensure, SerializableState>; +toSerializedFieldFormat(): {} | Ensure, SerializableRecord>; ``` Returns: -`{} | Ensure, SerializableState>` +`{} | Ensure, SerializableRecord>` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md index 6ca7a1a88b30..c4d09001087d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md @@ -13,7 +13,7 @@ constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ type: string | IAggType; @@ -27,6 +27,6 @@ constructor(indexPattern: IndexPattern, configStates: PickIndexPattern | | -| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | +| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("@kbn/common-utils").SerializableRecord | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | | opts | AggConfigsOptions | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md index a2028bb5c12c..7baa23fffe0d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md @@ -24,7 +24,7 @@ export declare class FilterManager implements PersistableStateService | [getAllMigrations](./kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md) | | () => {} | | | [inject](./kibana-plugin-plugins-data-public.filtermanager.inject.md) | | any | | | [migrateToLatest](./kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md) | | any | | -| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {} | | +| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {} | | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md index bab6452c3490..df5b4ea0a26c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md @@ -7,5 +7,5 @@ Signature: ```typescript -telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; +telemetry: (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {}; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index 4989b2b5ad58..f0261648e32a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; } ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index 07ede291e33d..77db30e96778 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -13,10 +13,11 @@ export declare type EmbeddableInput = { id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; - enhancements?: SerializableState; + enhancements?: SerializableRecord; disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext; }; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md index c54ebe4b1712..978873b6efbc 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

``` ## Properties diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md index 09ff48a92158..34462de42221 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

``` ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md index bfe1925a21f6..bc27adbed1d9 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md @@ -9,5 +9,5 @@ Contains the meta-data about the source of the expression. Signature: ```typescript -getExecutionContext: () => IExecutionContextContainer | undefined; +getExecutionContext: () => KibanaExecutionContext | undefined; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 38165a168331..8b876a7bcc3d 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties @@ -17,7 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) | () => KibanaExecutionContext | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 3e75e9ab3ef6..8a829659e6fb 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -29,7 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | -| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | +| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableRecord) => SerializableRecord;
} | | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md index 28d521f4b3fe..a8b55dae1592 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md @@ -8,6 +8,6 @@ ```typescript migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md index 1dee4a139c66..6d30d4569084 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md @@ -9,6 +9,6 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; }>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md index 504f5e2df7d6..c133621424b5 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -executionContext?: IExecutionContextContainer; +executionContext?: KibanaExecutionContext; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index a228628fece0..fcb0299e3fb6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -19,13 +19,13 @@ export interface IExpressionLoaderParams | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | | [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | -| [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) | IExecutionContextContainer | | +| [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) | KibanaExecutionContext | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | | [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | The flag to toggle on emitting partial results. By default, the partial results are disabled. | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | -| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | +| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableRecord | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | | [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) | number | Throttling of partial results in milliseconds. 0 is disabling the throttling. By default, it equals 1000. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md index 6b5fad950c4e..7b832af0e90d 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -searchContext?: SerializableState; +searchContext?: SerializableRecord; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md index b4ceb8f96d69..b692ee1611f9 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md @@ -9,5 +9,5 @@ Contains the meta-data about the source of the expression. Signature: ```typescript -getExecutionContext: () => IExecutionContextContainer | undefined; +getExecutionContext: () => KibanaExecutionContext | undefined; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 3b308ca46ab0..7a7ead6b9b15 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties @@ -17,7 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) | () => KibanaExecutionContext | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index 00c8aa63bfbd..3b3d60cc2736 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -29,7 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | -| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | +| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableRecord) => SerializableRecord;
} | | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md index 29031a9306b2..5d9410b62bb1 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md @@ -8,6 +8,6 @@ ```typescript migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md index c8132948a899..2a4f4dc7aab7 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md @@ -9,6 +9,6 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; }>; ``` diff --git a/docs/management/connectors/action-types/jira.asciidoc b/docs/management/connectors/action-types/jira.asciidoc index 368b11225654..aa6e92f965e8 100644 --- a/docs/management/connectors/action-types/jira.asciidoc +++ b/docs/management/connectors/action-types/jira.asciidoc @@ -16,8 +16,8 @@ Jira connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: Jira instance URL. Project key:: Jira project key. -Email (or username):: The account email (or username) for HTTP Basic authentication. -API token (or password):: Jira API authentication token (or password) for HTTP Basic authentication. +Email:: The account email for HTTP Basic authentication. +API token:: Jira API authentication token for HTTP Basic authentication. [float] [[jira-connector-networking-configuration]] diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index 0218bac58815..c7b12c4ac32f 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,9 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +TIP: When you upload GeoJSON or delimited files in {kib}, there is a file size +limit, which is configurable in <>. + [discrete] [[import-geospatial-privileges]] === Security privileges @@ -18,37 +21,36 @@ spaces in **{stack-manage-app}** in {kib}. For more information, see To upload GeoJSON files in {kib} with *Maps*, you must have: -* The `all` {kib} privilege for *Maps*. -* The `all` {kib} privilege for *Index Pattern Management*. -* The `create` and `create_index` index privileges for destination indices. -* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. +* The `all` {kib} privilege for *Maps* +* The `all` {kib} privilege for *{ipm-app}* +* The `create` and `create_index` index privileges for destination indices +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices -To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: +To upload delimited files (such as CSV, TSV, or JSON files) on the {kib} home page, you must also have: -* The `manage_pipeline` cluster privilege. -* The `read` {kib} privilege for *Machine Learning*. -* The `machine_learning_admin` or `machine_learning_user` role. +* The `all` {kib} privilege for *Discover* +* The `manage_pipeline` or `manage_ingest_pipelines` cluster privilege +* The `manage` index privilege for destination indices [discrete] -=== Upload CSV with latitude and longitude columns +=== Upload delimited files with latitude and longitude columns -*File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point. +On the {kib} home page, you can upload a file and import it into an {es} index with latitude and longitude columns combined into a `geo_point` field. -. Open the main menu, then click *Machine Learning*. -. Select the *Data Visualizer* tab, then click *Upload file*. -. Use the file chooser to select a CSV file. +. Go to the {kib} home page and click *Upload a file*. +. Select a file in one of the supported file formats. . Click *Import*. . Select the *Advanced* tab. . Set *Index name*. -. Click *Add combined field*, then click *Add geo point field*. +. If a combined `geo_point` field is not created automatically, click *Add combined field*, then click *Add geo point field*. . Fill out the form and click *Add*. . Click *Import*. [discrete] === Upload a GeoJSON file -*Upload GeoJSON* indexes GeoJSON features as a geo_point or geo_shape. +*Upload GeoJSON* indexes GeoJSON features as a `geo_point` or `geo_shape`. . <>. . Click *Add layer*. diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index fa89b7780e47..387d2308aa5e 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -22,9 +22,6 @@ Task Manager runs background tasks by polling for work on an interval. You can | `xpack.task_manager.request_capacity` | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. -| `xpack.task_manager.index` - | The name of the index used to store task information. Defaults to `.kibana_task_manager`. - | `xpack.task_manager.max_workers` | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. Starting in 8.0, it will not be possible to set the value greater than 100. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 880c98902983..25af236845ab 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -46,18 +46,30 @@ image::images/add-data-fleet.png[Add data using Fleet] [[upload-data-kibana]] === Upload a file -experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the {file-data-viz}. You can upload a file up to 100 MB. This value is configurable up to 1 GB in -<>. To upload a file with geospatial data, -refer to <>. +If you have a log file or delimited CSV, TSV, or JSON file, you can upload it, +view its fields and metrics, and optionally import it into {es}. -[role="screenshot"] -image::images/add-data-fv.png[File Data Visualizer] +NOTE: This feature is not intended for use as part of a repeated production +process, but rather for the initial exploration of your data. + +You can upload a file up to 100 MB. This value is configurable up to 1 GB in +<>. To upload a file with geospatial +data, refer to <>. +[role="screenshot"] +image::images/add-data-fv.png[Uploading a file in {kib}] +The {stack-security-features} provide roles and privileges that control which +users can upload files. To upload a file in {kib} and import it into an {es} +index, you'll need: -NOTE: This feature is not intended for use as part of a -repeated production process, but rather for the initial exploration of your data. +* `all` {kib} privileges for *Discover* +* `manage_pipeline` or `manage_ingest_pipelines` cluster privilege +* `create`, `create_index`, `manage`, and `read` index privileges for the index +You can manage your roles, privileges, and spaces in **{stack-manage-app}** in +{kib}. For more information, refer to +<>. [discrete] === Additional options for loading your data diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index a05ff1eeec4a..842d7cb054f3 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -17,7 +17,7 @@ if your data is stored in {es} and contains a time field, you can use the [role="screenshot"] image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* +You can also upload a CSV, NDJSON, or log file. The *{data-viz}* identifies the file format and field mappings. You can then optionally import that data into an {es} index. To change the default file size limit, see <>. diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 17eae59ff2f9..36745b913544 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -12,7 +12,7 @@ This has three major benefits: [IMPORTANT] ============================================== -Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. +Task definitions for alerts and actions are stored in the index `.kibana_task_manager`. You must have at least one replica of this index for production deployments. diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index cb999ed189d7..148e9f8ee14a 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -7,9 +7,9 @@ -- :keywords: analyst, concept, task, reporting -:description: {kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads with others, or on a website. +:description: {kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads with others, or on a website. -{kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads. +{kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads. You access the options from the *Share* menu in the toolbar. The sharing options include the following: @@ -34,9 +34,9 @@ You access the options from the *Share* menu in the toolbar. The sharing options Create and download PDF, PNG, or CSV reports of saved searches, dashboards, visualizations, and workpads. [[reporting-layout-sizing]] -The layout and size of the report depends on what you are sharing. +The layout and size of the report depends on what you are sharing. For saved searches, dashboards, and visualizations, the layout depends on the size of the panels. -For workpads, the layout depends on the size of the worksheet dimensions. +For workpads, the layout depends on the size of the worksheet dimensions. To change the output size, change the size of the browser, which resizes the shareable container before the report generates. It might take some trial and error before you're satisfied. @@ -67,6 +67,11 @@ NOTE: When you create a dashboard report that includes a data table or saved sea . To view and manage reports, open the main menu, then click *Stack Management > Reporting*. +NOTE: Reports are stored in {es} and managed by the `kibana-reporting` {ilm} +({ilm-init}) policy. By default, the policy stores reports forever. To learn +more about {ilm-init} policies, refer to the {es} +{ref}/index-lifecycle-management.html[{ilm-init} documentation]. + [float] [[share-a-direct-link]] == Share a direct link @@ -102,7 +107,7 @@ Create a JSON file for a workpad. . Open the main menu, then click *Canvas*. -. Open the workpad you want to share. +. Open the workpad you want to share. . From the toolbar, click *Share*, then select *Download as JSON*. @@ -110,7 +115,7 @@ Create a JSON file for a workpad. [[add-workpad-website]] == Share workpads on a website -beta[] *Canvas* allows you to create _shareables_, which are workpads that you download and securely share on a website. +beta[] *Canvas* allows you to create _shareables_, which are workpads that you download and securely share on a website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. . Open the main menu, then click *Canvas*. @@ -139,7 +144,7 @@ Some users might not have access to the dashboard or visualization. For more inf . Open the main menu, then open the dashboard or visualization you want to share. -. Click *Share > Embed code*. +. Click *Share > Embed code*. . Specify how you want to generate the code: diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json index 253fdd9ee6c8..fe909968bd8e 100644 --- a/examples/bfetch_explorer/tsconfig.json +++ b/examples/bfetch_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types", }, "include": [ "index.ts", @@ -14,6 +13,8 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, ] } diff --git a/examples/dashboard_embeddable_examples/tsconfig.json b/examples/dashboard_embeddable_examples/tsconfig.json index 86b35c5e4943..1c10d59de561 100644 --- a/examples/dashboard_embeddable_examples/tsconfig.json +++ b/examples/dashboard_embeddable_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,11 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../embeddable_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/developer_examples/tsconfig.json b/examples/developer_examples/tsconfig.json index 86b35c5e4943..23b24a38d1ae 100644 --- a/examples/developer_examples/tsconfig.json +++ b/examples/developer_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index a922c6defcd4..34c7c8e04467 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -15,6 +14,12 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../src/plugins/presentation_util/tsconfig.json" }, ] } diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index 4baebebcea42..e5b19e2c1457 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,10 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/inspector/tsconfig.json" } + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/inspector/tsconfig.json" }, + { "path": "../embeddable_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json index b4449819b25a..f3451b496caa 100644 --- a/examples/expressions_explorer/tsconfig.json +++ b/examples/expressions_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,9 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/inspector/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/hello_world/public/plugin.tsx b/examples/hello_world/public/plugin.tsx index cb648bffbb57..49acf07e6ae1 100755 --- a/examples/hello_world/public/plugin.tsx +++ b/examples/hello_world/public/plugin.tsx @@ -21,7 +21,7 @@ export class HelloWorldPlugin implements Plugin { id: 'helloWorld', title: 'Hello World', async mount({ element }: AppMountParameters) { - ReactDOM.render(

Hello World!
, element); + ReactDOM.render(
Hello World!
, element); return () => ReactDOM.unmountComponentAtNode(element); }, }); diff --git a/examples/hello_world/tsconfig.json b/examples/hello_world/tsconfig.json index 7fa03739119b..06d3953e4a6b 100644 --- a/examples/hello_world/tsconfig.json +++ b/examples/hello_world/tsconfig.json @@ -12,5 +12,9 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + ] } diff --git a/examples/index_pattern_field_editor_example/tsconfig.json b/examples/index_pattern_field_editor_example/tsconfig.json index 1f6d52ed5260..f11ca18fc1a8 100644 --- a/examples/index_pattern_field_editor_example/tsconfig.json +++ b/examples/index_pattern_field_editor_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,8 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_field_editor/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] -} \ No newline at end of file +} diff --git a/examples/locator_examples/public/locator.ts b/examples/locator_examples/public/locator.ts index 18caeca08564..878402d35727 100644 --- a/examples/locator_examples/public/locator.ts +++ b/examples/locator_examples/public/locator.ts @@ -6,16 +6,17 @@ * Side Public License, v 1. */ -import { SerializableState, MigrateFunction } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunction } from 'src/plugins/kibana_utils/common'; import { LocatorDefinition, LocatorPublic } from '../../../src/plugins/share/public'; export const HELLO_LOCATOR = 'HELLO_LOCATOR'; -export interface HelloLocatorV1Params extends SerializableState { +export interface HelloLocatorV1Params extends SerializableRecord { name: string; } -export interface HelloLocatorV2Params extends SerializableState { +export interface HelloLocatorV2Params extends SerializableRecord { firstName: string; lastName: string; } diff --git a/examples/locator_examples/tsconfig.json b/examples/locator_examples/tsconfig.json index 8aef3328b422..5010ad5a5fe0 100644 --- a/examples/locator_examples/tsconfig.json +++ b/examples/locator_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, ] } diff --git a/examples/locator_explorer/tsconfig.json b/examples/locator_explorer/tsconfig.json index 8aef3328b422..2fa75fc163fd 100644 --- a/examples/locator_explorer/tsconfig.json +++ b/examples/locator_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,9 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, + { "path": "../locator_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/preboot_example/tsconfig.json b/examples/preboot_example/tsconfig.json index d18953eadf33..e5b5eda0c647 100644 --- a/examples/preboot_example/tsconfig.json +++ b/examples/preboot_example/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/examples/routing_example/README.md b/examples/routing_example/README.md index 0a88707bf70b..1ac754050836 100644 --- a/examples/routing_example/README.md +++ b/examples/routing_example/README.md @@ -6,4 +6,4 @@ Read more: - [IRouter API Docs](../../docs/development/core/server/kibana-plugin-core-server.irouter.md) - [HttpHandler (core.http.fetch) API Docs](../../docs/development/core/public/kibana-plugin-core-public.httphandler.md) -- [Routing Conventions](../../STYLEGUIDE.md#api-endpoints) \ No newline at end of file +- [Routing Conventions](../../STYLEGUIDE.mdx#api-endpoints) \ No newline at end of file diff --git a/examples/routing_example/public/app.tsx b/examples/routing_example/public/app.tsx index 7dadde2936e8..c0f01b0b6d53 100644 --- a/examples/routing_example/public/app.tsx +++ b/examples/routing_example/public/app.tsx @@ -63,7 +63,8 @@ function RoutingExplorer({ }, { label: 'Conventions', - href: 'https://github.com/elastic/kibana/tree/master/STYLEGUIDE.md#api-endpoints', + href: + 'https://github.com/elastic/kibana/tree/master/STYLEGUIDE.mdx#api-endpoints', iconType: 'logoGithub', target: '_blank', size: 's', diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index 54ac800019f8..e47bf1c9bedb 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/screenshot_mode_example/tsconfig.json b/examples/screenshot_mode_example/tsconfig.json index dfb436e7377a..ef35abc699c6 100644 --- a/examples/screenshot_mode_example/tsconfig.json +++ b/examples/screenshot_mode_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,5 +12,11 @@ "../../typings/**/*" ], "exclude": [], - "references": [{ "path": "../../src/core/tsconfig.json" }] + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/screenshot_mode/tsconfig.json" }, + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + ] } diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 227fd7f1c626..87839f2037f9 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -11,5 +11,10 @@ "ui": true, "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils", "share"], "optionalPlugins": [], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Examples for using the data plugin search service. Includes examples for searching using the high level search source, or low-level search services, as well as integrating with search sessions." } diff --git a/examples/search_examples/tsconfig.json b/examples/search_examples/tsconfig.json index 2b7d86d76a8a..547952b8dd3d 100644 --- a/examples/search_examples/tsconfig.json +++ b/examples/search_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,12 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 442fa08022dc..fc266cbe2c83 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -17,5 +16,8 @@ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json index 253fdd9ee6c8..41d91ac4b9be 100644 --- a/examples/ui_action_examples/tsconfig.json +++ b/examples/ui_action_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,7 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, ] } diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json index b4449819b25a..6debf7c0c650 100644 --- a/examples/ui_actions_explorer/tsconfig.json +++ b/examples/ui_actions_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,8 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../ui_action_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/package.json b/package.json index 2058eed5f024..d7a072d1caef 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "33.2.0", + "@elastic/charts": "33.2.2", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.14", "@elastic/ems-client": "7.14.0", @@ -124,7 +124,6 @@ "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", - "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", "@kbn/config": "link:bazel-bin/packages/kbn-config", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index fcadedf8630f..5a8aa75ee255 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -13,7 +13,6 @@ filegroup( "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", "//packages/kbn-cli-dev-mode:build", - "//packages/kbn-common-utils:build", "//packages/kbn-config:build", "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", diff --git a/packages/elastic-datemath/BUILD.bazel b/packages/elastic-datemath/BUILD.bazel index 9244705c0155..04eb153bcc9e 100644 --- a/packages/elastic-datemath/BUILD.bazel +++ b/packages/elastic-datemath/BUILD.bazel @@ -39,6 +39,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -50,7 +51,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index dcfadbb7cf26..02465bebe519 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/elastic-safer-lodash-set/tsconfig.json b/packages/elastic-safer-lodash-set/tsconfig.json index 5a29c6ff2dd8..48aa6ed341d6 100644 --- a/packages/elastic-safer-lodash-set/tsconfig.json +++ b/packages/elastic-safer-lodash-set/tsconfig.json @@ -1,8 +1,6 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "incremental": false, - }, + "extends": "../../tsconfig.bazel.json", + "compilerOptions": {}, "include": [ "**/*", ], diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index 49c9c6f78325..5b9c38b16b53 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -55,6 +55,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -66,7 +67,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json index 8fa6be6fb3fc..4a132efa0911 100644 --- a/packages/kbn-ace/tsconfig.json +++ b/packages/kbn-ace/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-ace/src", diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index 9f78d99b7871..ca8cdcbffbb5 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -50,6 +50,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -59,6 +60,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -70,7 +72,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", root_dir = "src", source_map = True, diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index b24ed9ea137a..dfae54ad91c8 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "sourceMap": true, diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index 2ace393bcc4a..81189171412b 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index 314c7969b3f9..2f6da800d9dd 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-apm-utils/BUILD.bazel b/packages/kbn-apm-utils/BUILD.bazel index c75ffb420712..fdfd593476fe 100644 --- a/packages/kbn-apm-utils/BUILD.bazel +++ b/packages/kbn-apm-utils/BUILD.bazel @@ -40,6 +40,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -51,7 +52,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-apm-utils/tsconfig.json b/packages/kbn-apm-utils/tsconfig.json index 08fa0e8a4508..d7056cda6d71 100644 --- a/packages/kbn-apm-utils/tsconfig.json +++ b/packages/kbn-apm-utils/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 608c781017dd..e66a62178123 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -82,6 +82,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -93,7 +94,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", root_dir = "src", source_map = True, diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index 18fda8b97891..6ce2e21b8467 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-common-utils/BUILD.bazel b/packages/kbn-common-utils/BUILD.bazel deleted file mode 100644 index 699f65da408f..000000000000 --- a/packages/kbn-common-utils/BUILD.bazel +++ /dev/null @@ -1,89 +0,0 @@ -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" - -SOURCE_FILES = glob( - [ - "src/**/*.ts", - ], - exclude = ["**/*.test.*"], -) - -SRCS = SOURCE_FILES - -filegroup( - name = "srcs", - srcs = SRCS, -) - -NPM_MODULE_EXTRA_FILES = [ - "package.json", - "README.md" -] - -RUNTIME_DEPS = [ - "//packages/kbn-config-schema", - "@npm//tslib", -] - -TYPES_DEPS = [ - "//packages/kbn-config-schema", - "@npm//tslib", - "@npm//@types/jest", - "@npm//@types/node", -] - -jsts_transpiler( - name = "target_node", - srcs = SRCS, - build_pkg_name = package_name(), -) - -ts_config( - name = "tsconfig", - src = "tsconfig.json", - deps = [ - "//:tsconfig.base.json", - ], -) - -ts_project( - name = "tsc_types", - args = ['--pretty'], - srcs = SRCS, - deps = TYPES_DEPS, - declaration = True, - declaration_map = True, - emit_declaration_only = True, - incremental = False, - out_dir = "target_types", - source_map = True, - root_dir = "src", - tsconfig = ":tsconfig", -) - -js_library( - name = PKG_BASE_NAME, - srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], - package_name = PKG_REQUIRE_NAME, - visibility = ["//visibility:public"], -) - -pkg_npm( - name = "npm_module", - deps = [ - ":%s" % PKG_BASE_NAME, - ] -) - -filegroup( - name = "build", - srcs = [ - ":npm_module", - ], - visibility = ["//visibility:public"], -) diff --git a/packages/kbn-common-utils/README.md b/packages/kbn-common-utils/README.md deleted file mode 100644 index 7b64c9f18fe8..000000000000 --- a/packages/kbn-common-utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/common-utils - -Shared common (client and server sie) utilities shared across packages and plugins. \ No newline at end of file diff --git a/packages/kbn-common-utils/package.json b/packages/kbn-common-utils/package.json deleted file mode 100644 index a9679c2f0fb1..000000000000 --- a/packages/kbn-common-utils/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@kbn/common-utils", - "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 -} \ No newline at end of file diff --git a/packages/kbn-common-utils/src/json/typed_json.ts b/packages/kbn-common-utils/src/json/typed_json.ts deleted file mode 100644 index 06e99b9b65d5..000000000000 --- a/packages/kbn-common-utils/src/json/typed_json.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -export interface JsonObject { - [key: string]: JsonValue; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/packages/kbn-common-utils/tsconfig.json b/packages/kbn-common-utils/tsconfig.json deleted file mode 100644 index 7d1ecaa10a23..000000000000 --- a/packages/kbn-common-utils/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "incremental": false, - "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-common-utils/src", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/packages/kbn-config-schema/BUILD.bazel b/packages/kbn-config-schema/BUILD.bazel index 2a4718b87df1..70de78b7617c 100644 --- a/packages/kbn-config-schema/BUILD.bazel +++ b/packages/kbn-config-schema/BUILD.bazel @@ -51,6 +51,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index de9476bbbb35..79b652daf7ec 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index b11306be969b..75e4428ed2d7 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -68,6 +68,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -79,7 +80,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-config/tsconfig.json b/packages/kbn-config/tsconfig.json index 20855c607307..0971923a11a0 100644 --- a/packages/kbn-config/tsconfig.json +++ b/packages/kbn-config/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-common-utils/.babelrc b/packages/kbn-crypto/.babelrc similarity index 100% rename from packages/kbn-common-utils/.babelrc rename to packages/kbn-crypto/.babelrc diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 7419145f6288..0f35aab46107 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -1,6 +1,7 @@ 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-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" @@ -26,40 +27,43 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "@npm//node-forge", ] TYPES_DEPS = [ + "//packages/kbn-dev-utils", "@npm//@types/flot", "@npm//@types/jest", "@npm//@types/node", "@npm//@types/node-forge", - "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill", - "@npm//@emotion/react", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) 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, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -68,7 +72,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-crypto/package.json b/packages/kbn-crypto/package.json index bbeb57e5b7cc..8fa6cd3c232f 100644 --- a/packages/kbn-crypto/package.json +++ b/packages/kbn-crypto/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts" + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts" } diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index 8b7be5e8a3c9..0863fc3f530d 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-crypto/src", diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 65935a0d7386..502bcd05b74f 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -56,7 +56,6 @@ RUNTIME_DEPS = [ "@npm//globby", "@npm//load-json-file", "@npm//markdown-it", - "@npm//moment", "@npm//normalize-path", "@npm//rxjs", "@npm//tar", @@ -100,6 +99,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -111,7 +111,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-dev-utils/src/extract.ts b/packages/kbn-dev-utils/src/extract.ts index 05ad2b4bd99e..138e6bd67a50 100644 --- a/packages/kbn-dev-utils/src/extract.ts +++ b/packages/kbn-dev-utils/src/extract.ts @@ -12,12 +12,16 @@ import Path from 'path'; import { pipeline } from 'stream'; import { promisify } from 'util'; -import { lastValueFrom } from '@kbn/std'; import Tar from 'tar'; import Yauzl, { ZipFile, Entry } from 'yauzl'; import * as Rx from 'rxjs'; import { map, mergeMap, takeUntil } from 'rxjs/operators'; +const strComplete = (obs: Rx.Observable) => + new Promise((resolve, reject) => { + obs.subscribe({ complete: resolve, error: reject }); + }); + const asyncPipeline = promisify(pipeline); interface Options { @@ -36,6 +40,11 @@ interface Options { * Number of path segments to strip form paths in the archive, like --strip-components from tar */ stripComponents?: number; + + /** + * Write modified timestamps to extracted files + */ + setModifiedTimes?: Date; } /** @@ -43,7 +52,12 @@ interface Options { * for both archive types, only tested with familiar archives we create so might not * support some weird exotic zip features we don't use in our own snapshot/build tooling */ -export async function extract({ archivePath, targetDir, stripComponents = 0 }: Options) { +export async function extract({ + archivePath, + targetDir, + stripComponents = 0, + setModifiedTimes, +}: Options) { await Fs.mkdir(targetDir, { recursive: true }); if (archivePath.endsWith('.tar') || archivePath.endsWith('.tar.gz')) { @@ -98,7 +112,7 @@ export async function extract({ archivePath, targetDir, stripComponents = 0 }: O } // file entry - return openReadStream$(entry).pipe( + const writeFile$ = openReadStream$(entry).pipe( mergeMap(async (readStream) => { if (!readStream) { throw new Error('no readstream provided by yauzl'); @@ -106,15 +120,23 @@ export async function extract({ archivePath, targetDir, stripComponents = 0 }: O // write the file contents to disk await asyncPipeline(readStream, createWriteStream(fileName)); - // tell yauzl to read the next entry - zipFile.readEntry(); + + if (setModifiedTimes) { + // update the modified time of the file to match the zip entry + await Fs.utimes(fileName, setModifiedTimes, setModifiedTimes); + } }) ); + + // tell yauzl to read the next entry + zipFile.readEntry(); + + return writeFile$; }) ); // trigger the initial 'entry' event, happens async so the event will be delivered after the observable is subscribed zipFile.readEntry(); - await lastValueFrom(Rx.merge(entry$, error$)); + await strComplete(Rx.merge(entry$, error$)); } diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts index efe6d3087551..35c910c91110 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import moment from 'moment'; import * as Rx from 'rxjs'; import { filter, first, catchError, map } from 'rxjs/operators'; import exitHook from 'exit-hook'; @@ -199,8 +198,12 @@ export class ProcRunner { // tie into proc outcome$, remove from _procs on compete proc.outcome$.subscribe({ next: (code) => { - const duration = moment.duration(Date.now() - startMs); - this.log.info('[%s] exited with %s after %s', name, code, duration.humanize()); + this.log.info( + '[%s] exited with %s after %s seconds', + name, + code, + ((Date.now() - startMs) / 1000).toFixed(1) + ); }, complete: () => { remove(); diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 56843918d88b..3c22642edfaa 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,16 +1,14 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-dev-utils/src", "stripInternal": false, - "target": "ES2019", "types": [ "jest", "node" diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index cf609f126d7d..498d2140b91f 100644 --- a/packages/kbn-docs-utils/BUILD.bazel +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -47,6 +47,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -57,7 +58,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json index 57353d8847ae..e80808c6181f 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json @@ -4,4 +4,4 @@ "strictNullChecks": true, }, "include": ["./**/*"] -} \ No newline at end of file +} diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 9868c8b3d2bb..9b1d6994c1b2 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,9 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "target": "ES2019", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index 9b3db311afa2..b7040b584a31 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -59,6 +59,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,7 +70,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 2aa71fd23a71..bcf28a4976a1 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -101,7 +101,6 @@ const testRecords = [ it('indexes documents using the bulk client helper', async () => { const client = new MockClient(); - client.helpers.bulk.mockImplementation(async () => {}); const progress = new Progress(); const stats = createStats('test', log); @@ -185,11 +184,11 @@ it('indexes documents using the bulk client helper', async () => { "results": Array [ Object { "type": "return", - "value": Promise {}, + "value": undefined, }, Object { "type": "return", - "value": Promise {}, + "value": undefined, }, ], } diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 1bc93908a993..dce71fd6cd4a 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,9 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "target": "ES2019", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "sourceMap": true, diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 7d6e2b4683b9..d4a531d308f6 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -31,7 +31,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ - "//packages/kbn-common-utils", + "//packages/kbn-utility-types", "//packages/kbn-config-schema", "//packages/kbn-i18n", "@npm//@elastic/elasticsearch", @@ -42,7 +42,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-common-utils", + "//packages/kbn-utility-types", "//packages/kbn-i18n", "@npm//@elastic/elasticsearch", "@npm//@types/jest", @@ -84,6 +84,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -95,7 +96,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-es-query/src/kuery/ast/ast.ts b/packages/kbn-es-query/src/kuery/ast/ast.ts index 6f43098a752d..030b5a8f1c29 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; import { KueryNode, DslQuery, KueryParseOptions } from '../types'; diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.ts b/packages/kbn-es-query/src/kuery/node_types/named_arg.ts index b1b202e4323a..1892a4885b70 100644 --- a/packages/kbn-es-query/src/kuery/node_types/named_arg.ts +++ b/packages/kbn-es-query/src/kuery/node_types/named_arg.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import * as ast from '../ast'; import { nodeTypes } from '../node_types'; import { NamedArgTypeBuildNode } from './types'; diff --git a/packages/kbn-es-query/src/kuery/node_types/types.ts b/packages/kbn-es-query/src/kuery/node_types/types.ts index ea8eb5e8a061..7e6f45441855 100644 --- a/packages/kbn-es-query/src/kuery/node_types/types.ts +++ b/packages/kbn-es-query/src/kuery/node_types/types.ts @@ -10,7 +10,7 @@ * WARNING: these typings are incomplete */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { KueryNode } from '..'; import { IndexPatternBase } from '../..'; diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index 78e722884594..5b1f3be26313 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-es-query/src", diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 9487a2823268..83035b6315e6 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-es" + "outDir": "target/types" }, "include": [ "src/**/*" diff --git a/packages/kbn-expect/tsconfig.json b/packages/kbn-expect/tsconfig.json index 8c0d9f1e34bd..5ad392a7d8d5 100644 --- a/packages/kbn-expect/tsconfig.json +++ b/packages/kbn-expect/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, + "skipLibCheck": false }, "include": [ "expect.d.ts" diff --git a/packages/kbn-field-types/BUILD.bazel b/packages/kbn-field-types/BUILD.bazel index 829a1b11a0db..1fb97a5914ee 100644 --- a/packages/kbn-field-types/BUILD.bazel +++ b/packages/kbn-field-types/BUILD.bazel @@ -54,6 +54,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -65,7 +66,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-field-types/tsconfig.json b/packages/kbn-field-types/tsconfig.json index a7270134039b..150854076bbf 100644 --- a/packages/kbn-field-types/tsconfig.json +++ b/packages/kbn-field-types/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, "outDir": "./target_types", "declaration": true, "declarationMap": true, diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel index 1b3410c9f25e..62d5fb1d75a4 100644 --- a/packages/kbn-i18n/BUILD.bazel +++ b/packages/kbn-i18n/BUILD.bazel @@ -73,6 +73,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -84,7 +85,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 68c02adf9aae..2ac0b081b76d 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-i18n/src", diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index c29faf65638c..903f892b64f3 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -57,6 +57,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -68,7 +69,6 @@ ts_project( allow_js = True, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 011ed877146e..74ec484ea63e 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 053030a6f11a..0e5210bcc38a 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -45,6 +45,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -55,7 +56,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json index 7b8f25527549..3ee769739dfc 100644 --- a/packages/kbn-io-ts-utils/tsconfig.json +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "stripInternal": false, "declaration": true, "declarationMap": true, diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 1fd04604dbd2..7a9b472ca955 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -52,6 +52,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json index e3bcedd3de01..30a2e56602b6 100644 --- a/packages/kbn-legacy-logging/tsconfig.json +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-logging/BUILD.bazel b/packages/kbn-logging/BUILD.bazel index a5ef1581f41a..1a3fa851a395 100644 --- a/packages/kbn-logging/BUILD.bazel +++ b/packages/kbn-logging/BUILD.bazel @@ -41,6 +41,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -51,7 +52,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-logging/tsconfig.json b/packages/kbn-logging/tsconfig.json index 78985b823dd9..aaf79da229a8 100644 --- a/packages/kbn-logging/tsconfig.json +++ b/packages/kbn-logging/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index 7d7186068832..3cbf7c39421e 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -43,6 +43,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -53,7 +54,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json index cf1cca0f5a0f..159f40c6a7ca 100644 --- a/packages/kbn-mapbox-gl/tsconfig.json +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index 5f4351c89ff1..3656210cb6b1 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -81,6 +81,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -92,7 +93,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 03b017428beb..959051b17b78 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index 3809c2b33d50..ddf2a0551968 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -79,6 +79,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -89,7 +90,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 10ce4f7ab1d1..4772e00d5645 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -8,7 +8,7 @@ pageLoadAssetSize: charts: 95000 cloud: 21076 console: 46091 - core: 434325 + core: 435325 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index e7471846beca..047c98db8a80 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "./src", diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index cd18f5e14a73..c16862ee4f3c 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -65,6 +65,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -75,7 +76,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-plugin-generator/template/tsconfig.json.ejs b/packages/kbn-plugin-generator/template/tsconfig.json.ejs index 8a3ced743d0f..06ef420ae55e 100644 --- a/packages/kbn-plugin-generator/template/tsconfig.json.ejs +++ b/packages/kbn-plugin-generator/template/tsconfig.json.ejs @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index 9165fd21ebea..6a25803c8394 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "target": "ES2019", "declaration": true, diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel index 1a1f3453f768..9242701770a8 100644 --- a/packages/kbn-plugin-helpers/BUILD.bazel +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -56,6 +56,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -66,7 +67,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index 4348f1e1a751..22adf020187b 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "target": "ES2018", "declaration": true, diff --git a/packages/kbn-pm/tsconfig.json b/packages/kbn-pm/tsconfig.json index 558cff6556ff..f12e49fe28d4 100644 --- a/packages/kbn-pm/tsconfig.json +++ b/packages/kbn-pm/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-pm", + "outDir": "target/types", "types": [ "jest", "node" diff --git a/packages/kbn-rule-data-utils/.babelrc b/packages/kbn-rule-data-utils/.babelrc new file mode 100644 index 000000000000..7da72d177912 --- /dev/null +++ b/packages/kbn-rule-data-utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 62a48b66b4bc..bbc2d524102d 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-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-rule-data-utils" PKG_REQUIRE_NAME = "@kbn/rule-data-utils" @@ -21,37 +22,47 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-es-query", + "@npm//@elastic/elasticsearch", "@npm//tslib", "@npm//utility-types", - "@npm//@elastic/elasticsearch", ] TYPES_DEPS = [ + "//packages/kbn-es-query", + "@npm//@elastic/elasticsearch", + "@npm//tslib", + "@npm//utility-types", "@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", src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) 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", @@ -60,7 +71,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-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json index 42223e51ec2d..64bbb69d1440 100644 --- a/packages/kbn-rule-data-utils/package.json +++ b/packages/kbn-rule-data-utils/package.json @@ -1,7 +1,7 @@ { "name": "@kbn/rule-data-utils", - "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-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index a29c1023caf6..4b3f3fbb6f37 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -17,25 +17,18 @@ const CONSUMERS = `${KIBANA_NAMESPACE}.consumers` as const; const ECS_VERSION = 'ecs.version' as const; const EVENT_ACTION = 'event.action' as const; const EVENT_KIND = 'event.kind' as const; -const RULE_CATEGORY = 'rule.category' as const; -const RULE_CONSUMERS = 'rule.consumers' as const; -const RULE_ID = 'rule.id' as const; -const RULE_NAME = 'rule.name' as const; -const RULE_UUID = 'rule.uuid' as const; const SPACE_IDS = `${KIBANA_NAMESPACE}.space_ids` as const; const TAGS = 'tags' as const; const TIMESTAMP = '@timestamp' as const; const VERSION = `${KIBANA_NAMESPACE}.version` as const; +// Fields pertaining to the alert const ALERT_ACTION_GROUP = `${ALERT_NAMESPACE}.action_group` as const; const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_END = `${ALERT_NAMESPACE}.end` as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; -const ALERT_OWNER = `${ALERT_NAMESPACE}.owner` as const; -const ALERT_CONSUMERS = `${ALERT_NAMESPACE}.consumers` as const; -const ALERT_PRODUCER = `${ALERT_NAMESPACE}.producer` as const; const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const; const ALERT_RISK_SCORE = `${ALERT_NAMESPACE}.risk_score` as const; const ALERT_SEVERITY = `${ALERT_NAMESPACE}.severity` as const; @@ -49,8 +42,8 @@ const ALERT_WORKFLOW_REASON = `${ALERT_NAMESPACE}.workflow_reason` as const; const ALERT_WORKFLOW_STATUS = `${ALERT_NAMESPACE}.workflow_status` as const; const ALERT_WORKFLOW_USER = `${ALERT_NAMESPACE}.workflow_user` as const; +// Fields pertaining to the rule associated with the alert const ALERT_RULE_AUTHOR = `${ALERT_RULE_NAMESPACE}.author` as const; -const ALERT_RULE_CONSUMERS = `${ALERT_RULE_NAMESPACE}.consumers` as const; const ALERT_RULE_CREATED_AT = `${ALERT_RULE_NAMESPACE}.created_at` as const; const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const; const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const; @@ -59,6 +52,7 @@ const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const; const ALERT_RULE_ID = `${ALERT_RULE_NAMESPACE}.id` as const; const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; +const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const; const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const; const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const; @@ -75,6 +69,15 @@ const ALERT_RULE_TYPE_ID = `${ALERT_RULE_NAMESPACE}.rule_type_id` as const; const ALERT_RULE_UPDATED_AT = `${ALERT_RULE_NAMESPACE}.updated_at` as const; const ALERT_RULE_UPDATED_BY = `${ALERT_RULE_NAMESPACE}.updated_by` as const; const ALERT_RULE_VERSION = `${ALERT_RULE_NAMESPACE}.version` as const; +// the feature instantiating a rule type. +// Rule created in stack --> alerts +// Rule created in siem --> siem +const ALERT_RULE_CONSUMER = `${ALERT_RULE_NAMESPACE}.consumer` as const; +// the plugin that registered the rule type. +// Rule type apm.error_rate --> apm +// Rule type siem.signals --> siem +const ALERT_RULE_PRODUCER = `${ALERT_RULE_NAMESPACE}.producer` as const; +const ALERT_RULE_UUID = `${ALERT_RULE_NAMESPACE}.uuid` as const; const namespaces = { KIBANA_NAMESPACE, @@ -87,11 +90,6 @@ const fields = { ECS_VERSION, EVENT_KIND, EVENT_ACTION, - RULE_CATEGORY, - RULE_CONSUMERS, - RULE_ID, - RULE_NAME, - RULE_UUID, TAGS, TIMESTAMP, ALERT_ACTION_GROUP, @@ -100,13 +98,11 @@ const fields = { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_OWNER, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_REASON, ALERT_RISK_SCORE, ALERT_RULE_AUTHOR, - ALERT_RULE_CONSUMERS, ALERT_RULE_CREATED_AT, ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, @@ -141,6 +137,8 @@ const fields = { ALERT_WORKFLOW_REASON, ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_USER, + ALERT_RULE_UUID, + ALERT_RULE_CATEGORY, SPACE_IDS, VERSION, }; @@ -154,9 +152,8 @@ export { ALERT_ID, ALERT_NAMESPACE, ALERT_RULE_NAMESPACE, - ALERT_OWNER, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_REASON, ALERT_RISK_SCORE, ALERT_STATUS, @@ -164,7 +161,6 @@ export { ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_USER, ALERT_RULE_AUTHOR, - ALERT_RULE_CONSUMERS, ALERT_RULE_CREATED_AT, ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, @@ -200,11 +196,8 @@ export { EVENT_ACTION, EVENT_KIND, KIBANA_NAMESPACE, - RULE_CATEGORY, - RULE_CONSUMERS, - RULE_ID, - RULE_NAME, - RULE_UUID, + ALERT_RULE_UUID, + ALERT_RULE_CATEGORY, TAGS, TIMESTAMP, SPACE_IDS, diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json index 852393f01e59..be6095d187ef 100644 --- a/packages/kbn-rule-data-utils/tsconfig.json +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -1,20 +1,21 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "stripInternal": false, "declaration": true, "declarationMap": true, - "rootDir": "./src", + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "./target_types", + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", + "stripInternal": false, "types": [ "jest", "node" ] }, "include": [ - "./src/**/*.ts" + "src/**/*.ts" ] } diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index c29f6d6badc6..18c3b8f3ae3b 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -60,6 +60,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,6 +70,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -81,7 +83,6 @@ ts_project( declaration = True, declaration_dir = "target_types", declaration_map = True, - incremental = True, out_dir = "target_node", root_dir = "src", source_map = True, @@ -95,7 +96,6 @@ ts_project( deps = DEPS, allow_js = True, declaration = False, - incremental = True, out_dir = "target_web", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json index bab7b18c59cf..404043569aa9 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.browser.json", + "extends": "../../tsconfig.browser_bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "outDir": "./target_web", "declaration": false, "isolatedModules": true, diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json index bf402e93ffd6..484b639f9433 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "declarationDir": "./target_types", "outDir": "target_node", "declaration": true, diff --git a/packages/kbn-securitysolution-es-utils/BUILD.bazel b/packages/kbn-securitysolution-es-utils/BUILD.bazel index b69aa3df8ecb..97f2d9a41cd7 100644 --- a/packages/kbn-securitysolution-es-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-es-utils/BUILD.bazel @@ -44,7 +44,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -54,7 +55,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-es-utils/tsconfig.json b/packages/kbn-securitysolution-es-utils/tsconfig.json index be8848d781ca..906b01c943ab 100644 --- a/packages/kbn-securitysolution-es-utils/tsconfig.json +++ b/packages/kbn-securitysolution-es-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-hook-utils/BUILD.bazel b/packages/kbn-securitysolution-hook-utils/BUILD.bazel index 5bfe3d86867f..b5f3e0df2e0a 100644 --- a/packages/kbn-securitysolution-hook-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-hook-utils/BUILD.bazel @@ -45,7 +45,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -55,7 +56,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-hook-utils/tsconfig.json b/packages/kbn-securitysolution-hook-utils/tsconfig.json index 352dc086fec3..7e2cae223b14 100644 --- a/packages/kbn-securitysolution-hook-utils/tsconfig.json +++ b/packages/kbn-securitysolution-hook-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel index 8e4a464783fc..3c9818a58408 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -63,7 +64,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json index 3411ce2c93d0..5f69f2bd0e2e 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel index 99df07c3d8ea..bfdde4363985 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -54,6 +54,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json index d926653a4230..fb979c23df12 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel index d30eb913b1a5..fdf3ef64460d 100644 --- a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -52,6 +52,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json index 42a059439ecb..12de4ed6ccb7 100644 --- a/packages/kbn-securitysolution-io-ts-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel index b9326a502071..f6c81a9adb9a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -51,6 +51,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -61,7 +62,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json index 22718276926f..aa5aa4df550f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-api/BUILD.bazel b/packages/kbn-securitysolution-list-api/BUILD.bazel index 9055cf0804e4..726d75d8c86b 100644 --- a/packages/kbn-securitysolution-list-api/BUILD.bazel +++ b/packages/kbn-securitysolution-list-api/BUILD.bazel @@ -47,7 +47,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -57,7 +58,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-api/tsconfig.json b/packages/kbn-securitysolution-list-api/tsconfig.json index 4fad35e754f2..c2ac6d3d9228 100644 --- a/packages/kbn-securitysolution-list-api/tsconfig.json +++ b/packages/kbn-securitysolution-list-api/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-constants/BUILD.bazel b/packages/kbn-securitysolution-list-constants/BUILD.bazel index 8d6779bfa122..e56c96e0b5da 100644 --- a/packages/kbn-securitysolution-list-constants/BUILD.bazel +++ b/packages/kbn-securitysolution-list-constants/BUILD.bazel @@ -42,7 +42,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -52,7 +53,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-constants/tsconfig.json b/packages/kbn-securitysolution-list-constants/tsconfig.json index 84edcdd1d579..769b5df990e6 100644 --- a/packages/kbn-securitysolution-list-constants/tsconfig.json +++ b/packages/kbn-securitysolution-list-constants/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index 631e57958d5f..87075604a75c 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -54,7 +54,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-hooks/tsconfig.json b/packages/kbn-securitysolution-list-hooks/tsconfig.json index 9e99c22ea9a0..9b09c02bd4aa 100644 --- a/packages/kbn-securitysolution-list-hooks/tsconfig.json +++ b/packages/kbn-securitysolution-list-hooks/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index cb6ddbd7f91d..b35b13004b1a 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -49,7 +49,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -59,7 +60,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-utils/tsconfig.json b/packages/kbn-securitysolution-list-utils/tsconfig.json index c46215944589..da48f4af29ea 100644 --- a/packages/kbn-securitysolution-list-utils/tsconfig.json +++ b/packages/kbn-securitysolution-list-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel index 5cf1081bdd32..0c5ef200fc96 100644 --- a/packages/kbn-securitysolution-t-grid/BUILD.bazel +++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel @@ -59,7 +59,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,6 +70,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -80,7 +82,6 @@ ts_project( declaration = True, declaration_dir = "target_types", declaration_map = True, - incremental = True, out_dir = "target_node", root_dir = "src", source_map = True, @@ -94,7 +95,6 @@ ts_project( deps = DEPS, allow_js = True, declaration = False, - incremental = True, out_dir = "target_web", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json index a5183ba4fd45..e951765c4b99 100644 --- a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json +++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.browser.json", + "extends": "../../tsconfig.browser_bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "outDir": "./target_web", "declaration": false, "isolatedModules": true, diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json index 8cda578edede..1d5957e516e0 100644 --- a/packages/kbn-securitysolution-t-grid/tsconfig.json +++ b/packages/kbn-securitysolution-t-grid/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 6084480ef6a1..41fb97bc6079 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -44,7 +44,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -54,7 +55,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-utils/tsconfig.json b/packages/kbn-securitysolution-utils/tsconfig.json index 783e67666d8b..3894b53d6cff 100644 --- a/packages/kbn-securitysolution-utils/tsconfig.json +++ b/packages/kbn-securitysolution-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel index 2e1a33f64743..609fe6d00f17 100644 --- a/packages/kbn-server-http-tools/BUILD.bazel +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -61,6 +61,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -72,7 +73,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index 643b932073c9..e378e41c3828 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,11 +1,10 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, - "outDir": "./target", + "outDir": "./target/types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-server-http-tools/src", diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index 147b72c65f95..9f8a9f34061d 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -57,6 +57,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -68,7 +69,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 947744bc68d4..447a2084926c 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel index bcc5a87a1e79..571d3c061c13 100644 --- a/packages/kbn-std/BUILD.bazel +++ b/packages/kbn-std/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index 3cff7fbd258a..2674ca26e96d 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-storybook/.babelrc b/packages/kbn-storybook/.babelrc new file mode 100644 index 000000000000..7da72d177912 --- /dev/null +++ b/packages/kbn-storybook/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index e18256aeb8da..dce2a6b3010f 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -1,14 +1,14 @@ 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-storybook" PKG_REQUIRE_NAME = "@kbn/storybook" SOURCE_FILES = glob( [ - "lib/**/*.ts", - "lib/**/*.tsx", - "*.ts", + "src/**/*.ts", + "src/**/*.tsx", ], exclude = ["**/*.test.*"], ) @@ -28,7 +28,7 @@ NPM_MODULE_EXTRA_FILES = [ "preset.js", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-ui-shared-deps", "@npm//@storybook/addons", @@ -45,31 +45,47 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", "@npm//@types/loader-utils", "@npm//@types/node", + "@npm//@types/react", "@npm//@types/webpack", "@npm//@types/webpack-merge", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) 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, + out_dir = "target_types", + root_dir = "src", source_map = True, tsconfig = ":tsconfig", ) @@ -77,7 +93,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = [":tsc"] + DEPS, + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f3c12f19a079..5360e4a5c547 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index be0012a3818b..a0170dad1a31 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -7,11 +7,11 @@ */ // eslint-disable-next-line -const webpackConfig = require('./target/webpack.config').default; +const webpackConfig = require('./target_node/webpack.config'); module.exports = { managerEntries: (entry = []) => { - return [...entry, require.resolve('./target/lib/register')]; + return [...entry, require.resolve('./target_node/lib/register')]; }, webpackFinal: (config) => { return webpackConfig({ config }); diff --git a/packages/kbn-storybook/ignore_not_found_export_plugin.ts b/packages/kbn-storybook/src/ignore_not_found_export_plugin.ts similarity index 100% rename from packages/kbn-storybook/ignore_not_found_export_plugin.ts rename to packages/kbn-storybook/src/ignore_not_found_export_plugin.ts diff --git a/packages/kbn-storybook/index.ts b/packages/kbn-storybook/src/index.ts similarity index 88% rename from packages/kbn-storybook/index.ts rename to packages/kbn-storybook/src/index.ts index 16ac1c6e10c4..1694c995c577 100644 --- a/packages/kbn-storybook/index.ts +++ b/packages/kbn-storybook/src/index.ts @@ -8,3 +8,4 @@ export { defaultConfig } from './lib/default_config'; export { runStorybookCli } from './lib/run_storybook_cli'; +export { default as WebpackConfig } from './webpack.config'; diff --git a/packages/kbn-storybook/lib/constants.ts b/packages/kbn-storybook/src/lib/constants.ts similarity index 100% rename from packages/kbn-storybook/lib/constants.ts rename to packages/kbn-storybook/src/lib/constants.ts diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/src/lib/default_config.ts similarity index 100% rename from packages/kbn-storybook/lib/default_config.ts rename to packages/kbn-storybook/src/lib/default_config.ts diff --git a/packages/kbn-storybook/lib/register.ts b/packages/kbn-storybook/src/lib/register.ts similarity index 100% rename from packages/kbn-storybook/lib/register.ts rename to packages/kbn-storybook/src/lib/register.ts diff --git a/packages/kbn-storybook/lib/register_theme_switcher_addon.ts b/packages/kbn-storybook/src/lib/register_theme_switcher_addon.ts similarity index 100% rename from packages/kbn-storybook/lib/register_theme_switcher_addon.ts rename to packages/kbn-storybook/src/lib/register_theme_switcher_addon.ts diff --git a/packages/kbn-storybook/lib/run_storybook_cli.ts b/packages/kbn-storybook/src/lib/run_storybook_cli.ts similarity index 100% rename from packages/kbn-storybook/lib/run_storybook_cli.ts rename to packages/kbn-storybook/src/lib/run_storybook_cli.ts diff --git a/packages/kbn-storybook/lib/theme_switcher.tsx b/packages/kbn-storybook/src/lib/theme_switcher.tsx similarity index 100% rename from packages/kbn-storybook/lib/theme_switcher.tsx rename to packages/kbn-storybook/src/lib/theme_switcher.tsx diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/src/typings.ts similarity index 100% rename from packages/kbn-storybook/typings.ts rename to packages/kbn-storybook/src/typings.ts diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts similarity index 100% rename from packages/kbn-storybook/webpack.config.ts rename to packages/kbn-storybook/src/webpack.config.ts diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 1f6886c45c50..0ccf3e78c828 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,15 +1,19 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "target", - "skipLibCheck": true, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "target_types", + "rootDir": "src", + "skipLibCheck": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] + "include": [ + "src/**/*.ts", "src/**/*.tsx" + ] } diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index f90ca9b22dc9..1183de258642 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -59,6 +59,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -70,7 +71,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 1f1b4aea5d5c..034d7c0c6e74 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "rootDir": "src", diff --git a/packages/kbn-test-subj-selector/tsconfig.json b/packages/kbn-test-subj-selector/tsconfig.json index a1e1c1af372c..a59007394616 100644 --- a/packages/kbn-test-subj-selector/tsconfig.json +++ b/packages/kbn-test-subj-selector/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test-subj-selector" + "outDir": "target/types" }, "include": [ "index.d.ts" diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 34497ca141c5..36e81df6d8c3 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -110,6 +110,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -121,7 +122,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index e08695b334e1..734e26c5199d 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,6 +22,11 @@ interface UrlParam { username?: string; } +interface App { + pathname?: string; + hash?: string; +} + /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -41,11 +46,11 @@ interface UrlParam { * @return {string} */ -function getUrl(config: UrlParam, app: UrlParam) { +function getUrl(config: UrlParam, app: App) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 374139d965e3..7ba83019b007 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "stripInternal": true, "rootDir": "src", diff --git a/packages/kbn-tinymath/tsconfig.json b/packages/kbn-tinymath/tsconfig.json index 73133b7318a0..748eb53a69e3 100644 --- a/packages/kbn-tinymath/tsconfig.json +++ b/packages/kbn-tinymath/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "include": ["index.d.ts"] } diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index fa89bc5029cc..be346f8321fa 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -64,6 +64,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -75,7 +76,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 97b6d16e3214..4de4b44196dd 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -27,6 +27,11 @@ describe('createRouter', () => { rangeTo: t.string, }), }), + defaults: { + query: { + rangeFrom: 'now-30m', + }, + }, children: [ { path: '/services', @@ -164,6 +169,20 @@ describe('createRouter', () => { router.getParams('/service-map', history.location, true); }).not.toThrowError(); }); + + it('applies defaults', () => { + history.push('/services?rangeTo=now&transactionType=request'); + + const topLevelParams = router.getParams('/', history.location); + + expect(topLevelParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-30m', + rangeTo: 'now', + }, + }); + }); }); describe('matchRoutes', () => { @@ -181,6 +200,19 @@ describe('createRouter', () => { router.matchRoutes('/traces', history.location); }).toThrowError('No matching route found for /traces'); }); + + it('applies defaults', () => { + history.push('/services?rangeTo=now&transactionType=request'); + + const matches = router.matchRoutes('/', history.location); + + expect(matches[1]?.match.params).toEqual({ + query: { + rangeFrom: 'now-30m', + rangeTo: 'now', + }, + }); + }); }); describe('link', () => { @@ -241,5 +273,17 @@ describe('createRouter', () => { } as any); }).toThrowError(); }); + + it('applies defaults', () => { + const href = router.link('/traces', { + // @ts-ignore + query: { + rangeTo: 'now', + aggregationType: 'avg', + }, + }); + + expect(href).toEqual('/traces?aggregationType=avg&rangeFrom=now-30m&rangeTo=now'); + }); }); }); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 4a8b89560d51..846808cb798f 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -76,10 +76,12 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router { const params: { path?: Record; query?: Record } | undefined = args[0]; - const paramsWithDefaults = merge({ path: {}, query: {} }, params); + const paramsWithBuiltInDefaults = merge({ path: {}, query: {} }, params); path = path .split('/') .map((part) => { - return part.startsWith(':') ? paramsWithDefaults.path[part.split(':')[1]] : part; + return part.startsWith(':') ? paramsWithBuiltInDefaults.path[part.split(':')[1]] : part; }) .join('/'); @@ -125,15 +127,25 @@ export function createRouter(routes: TRoutes): Router { + return routesByReactRouterConfig.get(match.route)!; + }); + const validationType = mergeRt( ...(compact( - matches.map((match) => { - return routesByReactRouterConfig.get(match.route)?.params; + matchedRoutes.map((match) => { + return match.params; }) ) as [any, any]) ); - const validation = validationType.decode(paramsWithDefaults); + const paramsWithRouteDefaults = merge( + {}, + ...matchedRoutes.map((route) => route.defaults ?? {}), + paramsWithBuiltInDefaults + ); + + const validation = validationType.decode(paramsWithRouteDefaults); if (isLeft(validation)) { throw new Error(PathReporter.report(validation).join('\n')); @@ -141,7 +153,7 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router { const matches = matchRoutes(...args); return matches.length - ? merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)) + ? merge( + { path: {}, query: {} }, + ...matches.map((match) => merge({}, match.route?.defaults ?? {}, match.match.params)) + ) : undefined; }, matchRoutes: (...args: any[]) => { diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index f1609d870962..1d6a77d36000 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -56,6 +56,7 @@ interface PlainRoute { element: ReactElement; children?: PlainRoute[]; params?: t.Type; + defaults?: Record>; } interface ReadonlyPlainRoute { @@ -63,6 +64,7 @@ interface ReadonlyPlainRoute { readonly element: ReactElement; readonly children?: readonly ReadonlyPlainRoute[]; readonly params?: t.Type; + readonly defaults?: Record>; } export type Route = PlainRoute | ReadonlyPlainRoute; diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index bee43cac2df0..8e17781119ee 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "sourceMap": true, diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index 426f8d0b7485..352fd4890734 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -81,6 +81,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -92,7 +93,6 @@ ts_project( allow_js = True, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index 0fd49ede2183..90a89ac580a4 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index 047cbd77f81c..8b63eea537aa 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -7,6 +7,7 @@ PKG_REQUIRE_NAME = "@kbn/utility-types" SOURCE_FILES = glob([ "src/jest/index.ts", + "src/serializable/**", "src/index.ts" ]) @@ -44,6 +45,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -55,7 +57,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-utility-types/src/index.ts b/packages/kbn-utility-types/src/index.ts index 1f5d95e316e1..921f056c6b75 100644 --- a/packages/kbn-utility-types/src/index.ts +++ b/packages/kbn-utility-types/src/index.ts @@ -9,6 +9,14 @@ import { PromiseType } from 'utility-types'; export { $Values, Assign, Class, Optional, Required } from 'utility-types'; +export type { + JsonArray, + JsonValue, + JsonObject, + SerializableRecord, + Serializable, +} from './serializable'; + /** * A type that may or may not be a `Promise`. */ diff --git a/src/core/types/serializable.ts b/packages/kbn-utility-types/src/serializable/index.ts similarity index 74% rename from src/core/types/serializable.ts rename to packages/kbn-utility-types/src/serializable/index.ts index 19f9c6cb2154..3a248f9aea2e 100644 --- a/src/core/types/serializable.ts +++ b/packages/kbn-utility-types/src/serializable/index.ts @@ -6,11 +6,21 @@ * Side Public License, v 1. */ +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface JsonArray extends Array {} + export type Serializable = | string | number | boolean | null + | undefined | SerializableArray | SerializableRecord; diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 92fd810b389c..997bcf9e0c45 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-utils/BUILD.bazel b/packages/kbn-utils/BUILD.bazel index c4ded75eda63..c2f82d65d331 100644 --- a/packages/kbn-utils/BUILD.bazel +++ b/packages/kbn-utils/BUILD.bazel @@ -49,6 +49,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -60,7 +61,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-utils/src/package_json/index.test.ts b/packages/kbn-utils/src/package_json/index.test.ts index f6d7e1f2f611..f8d1b5d25fa1 100644 --- a/packages/kbn-utils/src/package_json/index.test.ts +++ b/packages/kbn-utils/src/package_json/index.test.ts @@ -6,15 +6,8 @@ * Side Public License, v 1. */ -import path from 'path'; -import { kibanaPackageJson } from './'; +import { kibanaPackageJson } from './index'; it('parses package.json', () => { expect(kibanaPackageJson.name).toEqual('kibana'); }); - -it('includes __dirname and __filename', () => { - const root = path.resolve(__dirname, '../../../../'); - expect(kibanaPackageJson.__filename).toEqual(path.resolve(root, 'package.json')); - expect(kibanaPackageJson.__dirname).toEqual(root); -}); diff --git a/packages/kbn-utils/src/package_json/index.ts b/packages/kbn-utils/src/package_json/index.ts index d9304cee2ca3..fbd554c8c2c0 100644 --- a/packages/kbn-utils/src/package_json/index.ts +++ b/packages/kbn-utils/src/package_json/index.ts @@ -6,13 +6,11 @@ * Side Public License, v 1. */ -import { dirname, resolve } from 'path'; +import Path from 'path'; import { REPO_ROOT } from '../repo_root'; export const kibanaPackageJson = { - __filename: resolve(REPO_ROOT, 'package.json'), - __dirname: dirname(resolve(REPO_ROOT, 'package.json')), - ...require(resolve(REPO_ROOT, 'package.json')), + ...require(Path.resolve(REPO_ROOT, 'package.json')), }; export const isKibanaDistributable = () => { diff --git a/packages/kbn-utils/tsconfig.json b/packages/kbn-utils/tsconfig.json index cf546e2a1395..85c26f42a695 100644 --- a/packages/kbn-utils/tsconfig.json +++ b/packages/kbn-utils/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-utils/src", diff --git a/packages/kbn-common-utils/src/json/index.ts b/scripts/convert_ts_projects.js similarity index 79% rename from packages/kbn-common-utils/src/json/index.ts rename to scripts/convert_ts_projects.js index 96c94df1bb48..65053db0d0bd 100644 --- a/packages/kbn-common-utils/src/json/index.ts +++ b/scripts/convert_ts_projects.js @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { JsonArray, JsonValue, JsonObject } from './typed_json'; +require('../src/setup_node_env'); +require('../src/dev/typescript/convert_all_to_composite'); diff --git a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap index f5d3b837ce71..16b67bfa0584 100644 --- a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap +++ b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap @@ -27,7 +27,7 @@ exports[`StatusTable renders when statuses is provided 1`] = ` Object { "id": "plugin:1", "state": Object { - "id": "green", + "id": "available", "message": "Ready", "title": "green", "uiColor": "secondary", diff --git a/src/core/public/core_app/status/components/server_status.test.tsx b/src/core/public/core_app/status/components/server_status.test.tsx index f0a9a9a88bc4..3aa41827ff40 100644 --- a/src/core/public/core_app/status/components/server_status.test.tsx +++ b/src/core/public/core_app/status/components/server_status.test.tsx @@ -12,7 +12,7 @@ import { ServerStatus } from './server_status'; import { FormattedStatus } from '../lib'; const getStatus = (parts: Partial = {}): FormattedStatus['state'] => ({ - id: 'green', + id: 'available', title: 'Green', uiColor: 'secondary', message: '', @@ -29,7 +29,7 @@ describe('ServerStatus', () => { it('renders correctly for red state', () => { const status = getStatus({ - id: 'red', + id: 'unavailable', title: 'Red', }); const component = mount(); diff --git a/src/core/public/core_app/status/components/status_table.test.tsx b/src/core/public/core_app/status/components/status_table.test.tsx index 6f6324bf809e..af7d33bee5ed 100644 --- a/src/core/public/core_app/status/components/status_table.test.tsx +++ b/src/core/public/core_app/status/components/status_table.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { StatusTable } from './status_table'; const state = { - id: 'green', + id: 'available' as const, uiColor: 'secondary', message: 'Ready', title: 'green', diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts index 349f20a2385c..e412192ea00e 100644 --- a/src/core/public/core_app/status/lib/load_status.test.ts +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -17,36 +17,37 @@ const mockedResponse: StatusResponse = { version: { number: '8.0.0', build_hash: '9007199254740991', - build_number: '12', - build_snapshot: 'XXXXXXXX', + build_number: 12, + build_snapshot: false, }, status: { overall: { - id: 'overall', - state: 'yellow', - title: 'Yellow', - message: 'yellow', - uiColor: 'secondary', + level: 'degraded', + summary: 'yellow', }, - statuses: [ - { - id: 'plugin:1', - state: 'green', - title: 'Green', - message: 'Ready', - uiColor: 'secondary', + core: { + elasticsearch: { + level: 'available', + summary: 'Elasticsearch is available', }, - { - id: 'plugin:2', - state: 'yellow', - title: 'Yellow', - message: 'Something is weird', - uiColor: 'warning', + savedObjects: { + level: 'available', + summary: 'SavedObjects service has completed migrations and is available', + }, + }, + plugins: { + '1': { + level: 'available', + summary: 'Ready', + }, + '2': { + level: 'degraded', + summary: 'Something is weird', }, - ], + }, }, metrics: { - collected_at: new Date('2020-01-01 01:00:00'), + last_updated: '2020-01-01 01:00:00', collection_interval_in_millis: 1000, os: { platform: 'darwin' as const, @@ -80,6 +81,7 @@ const mockedResponse: StatusResponse = { disconnects: 1, total: 400, statusCodes: {}, + status_codes: {}, }, concurrent_connections: 1, }, @@ -148,13 +150,36 @@ describe('response processing', () => { test('includes the plugin statuses', async () => { const data = await loadStatus({ http, notifications }); expect(data.statuses).toEqual([ + { + id: 'core:elasticsearch', + state: { + id: 'available', + title: 'Green', + message: 'Elasticsearch is available', + uiColor: 'secondary', + }, + }, + { + id: 'core:savedObjects', + state: { + id: 'available', + title: 'Green', + message: 'SavedObjects service has completed migrations and is available', + uiColor: 'secondary', + }, + }, { id: 'plugin:1', - state: { id: 'green', title: 'Green', message: 'Ready', uiColor: 'secondary' }, + state: { id: 'available', title: 'Green', message: 'Ready', uiColor: 'secondary' }, }, { id: 'plugin:2', - state: { id: 'yellow', title: 'Yellow', message: 'Something is weird', uiColor: 'warning' }, + state: { + id: 'degraded', + title: 'Yellow', + message: 'Something is weird', + uiColor: 'warning', + }, }, ]); }); @@ -162,10 +187,10 @@ describe('response processing', () => { test('includes the serverState', async () => { const data = await loadStatus({ http, notifications }); expect(data.serverState).toEqual({ - id: 'yellow', + id: 'degraded', title: 'Yellow', message: 'yellow', - uiColor: 'secondary', + uiColor: 'warning', }); }); diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts index 0748c3dfe1de..a5cc18ffd6c1 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { UnwrapPromise } from '@kbn/utility-types'; -import type { ServerStatus, StatusResponse } from '../../../../types/status'; +import type { StatusResponse, ServiceStatus, ServiceStatusLevel } from '../../../../types/status'; import type { HttpSetup } from '../../../http'; import type { NotificationsSetup } from '../../../notifications'; import type { DataType } from '../lib'; @@ -22,13 +22,18 @@ export interface Metric { export interface FormattedStatus { id: string; state: { - id: string; + id: ServiceStatusLevel; title: string; message: string; uiColor: string; }; } +interface StatusUIAttributes { + title: string; + uiColor: string; +} + /** * Returns an object of any keys that should be included for metrics. */ @@ -86,18 +91,47 @@ function formatMetrics({ metrics }: StatusResponse): Metric[] { /** * Reformat the backend data to make the frontend views simpler. */ -function formatStatus(status: ServerStatus): FormattedStatus { +function formatStatus(id: string, status: ServiceStatus): FormattedStatus { + const { title, uiColor } = STATUS_LEVEL_UI_ATTRS[status.level]; + return { - id: status.id, + id, state: { - id: status.state, - title: status.title, - message: status.message, - uiColor: status.uiColor, + id: status.level, + message: status.summary, + title, + uiColor, }, }; } +const STATUS_LEVEL_UI_ATTRS: Record = { + critical: { + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + uiColor: 'danger', + }, + unavailable: { + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + uiColor: 'danger', + }, + degraded: { + title: i18n.translate('core.status.yellowTitle', { + defaultMessage: 'Yellow', + }), + uiColor: 'warning', + }, + available: { + title: i18n.translate('core.status.greenTitle', { + defaultMessage: 'Green', + }), + uiColor: 'secondary', + }, +}; + /** * Get the status from the server API and format it for display. */ @@ -111,7 +145,7 @@ export async function loadStatus({ let response: StatusResponse; try { - response = await http.get('/api/status'); + response = await http.get('/api/status', { query: { v8format: true } }); } catch (e) { // API returns a 503 response if not all services are available. // In this case, we want to treat this as a successful API call, so that we can @@ -144,8 +178,15 @@ export async function loadStatus({ return { name: response.name, version: response.version, - statuses: response.status.statuses.map(formatStatus), - serverState: formatStatus(response.status.overall).state, + statuses: [ + ...Object.entries(response.status.core).map(([serviceName, status]) => + formatStatus(`core:${serviceName}`, status) + ), + ...Object.entries(response.status.plugins).map(([pluginName, status]) => + formatStatus(`plugin:${pluginName}`, status) + ), + ], + serverState: formatStatus('overall', response.status.overall).state, metrics: formatMetrics(response), }; } diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index c80c2e3f4977..afb8aec31ccc 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -19,7 +19,6 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { renderingServiceMock } from './rendering/rendering_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; -import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); @@ -112,14 +111,6 @@ jest.doMock('./integrations', () => ({ IntegrationsService: IntegrationsServiceConstructor, })); -export const MockExecutionContextService = executionContextServiceMock.create(); -export const ExecutionContextServiceConstructor = jest - .fn() - .mockImplementation(() => MockExecutionContextService); -jest.doMock('./execution_context', () => ({ - ExecutionContextService: ExecutionContextServiceConstructor, -})); - export const MockCoreApp = coreAppMock.create(); export const CoreAppConstructor = jest.fn().mockImplementation(() => MockCoreApp); jest.doMock('./core_app', () => ({ diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index efafb25da27e..8ead0f50785b 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -30,7 +30,6 @@ import { RenderingServiceConstructor, IntegrationsServiceConstructor, MockIntegrationsService, - MockExecutionContextService, CoreAppConstructor, MockCoreApp, } from './core_system.test.mocks'; @@ -183,11 +182,6 @@ describe('#setup()', () => { await setupCore(); expect(MockCoreApp.setup).toHaveBeenCalledTimes(1); }); - - it('calls executionContext.setup()', async () => { - await setupCore(); - expect(MockExecutionContextService.setup).toHaveBeenCalledTimes(1); - }); }); describe('#start()', () => { @@ -275,11 +269,6 @@ describe('#start()', () => { await startCore(); expect(MockCoreApp.start).toHaveBeenCalledTimes(1); }); - - it('calls executionContext.start()', async () => { - await startCore(); - expect(MockExecutionContextService.start).toHaveBeenCalledTimes(1); - }); }); describe('#stop()', () => { @@ -338,14 +327,6 @@ describe('#stop()', () => { expect(MockCoreApp.stop).toHaveBeenCalled(); }); - it('calls executionContext.stop()', () => { - const coreSystem = createCoreSystem(); - - expect(MockExecutionContextService.stop).not.toHaveBeenCalled(); - coreSystem.stop(); - expect(MockExecutionContextService.stop).toHaveBeenCalled(); - }); - it('clears the rootDomElement', async () => { const rootDomElement = document.createElement('div'); const coreSystem = createCoreSystem({ diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 43e7d443f5c0..e5dcd8f817a0 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -29,7 +29,6 @@ import { SavedObjectsService } from './saved_objects'; import { IntegrationsService } from './integrations'; import { DeprecationsService } from './deprecations'; import { CoreApp } from './core_app'; -import { ExecutionContextService } from './execution_context'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; interface Params { @@ -84,7 +83,6 @@ export class CoreSystem { private readonly integrations: IntegrationsService; private readonly coreApp: CoreApp; private readonly deprecations: DeprecationsService; - private readonly executionContext: ExecutionContextService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; @@ -120,7 +118,6 @@ export class CoreSystem { this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); - this.executionContext = new ExecutionContextService(); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); @@ -140,7 +137,6 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - this.executionContext.setup(); const application = this.application.setup({ http }); this.coreApp.setup({ application, http, injectedMetadata, notifications }); @@ -205,7 +201,6 @@ export class CoreSystem { notifications, }); const deprecations = this.deprecations.start({ http }); - const executionContext = this.executionContext.start(); this.coreApp.start({ application, docLinks, http, notifications, uiSettings }); @@ -222,7 +217,6 @@ export class CoreSystem { uiSettings, fatalErrors, deprecations, - executionContext, }; await this.plugins.start(core); @@ -266,7 +260,6 @@ export class CoreSystem { this.i18n.stop(); this.application.stop(); this.deprecations.stop(); - this.executionContext.stop(); this.rootDomElement.textContent = ''; } } diff --git a/src/core/public/execution_context/execution_context_container.test.ts b/src/core/public/execution_context/execution_context_container.test.ts index a4ee355ab40a..5e4e34d102e5 100644 --- a/src/core/public/execution_context/execution_context_container.test.ts +++ b/src/core/public/execution_context/execution_context_container.test.ts @@ -29,6 +29,30 @@ describe('KibanaExecutionContext', () => { `); }); + it('includes a parent context to string representation', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', + }; + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: parentContext, + }; + + const value = new ExecutionContextContainer(context).toHeader(); + expect(value).toMatchInlineSnapshot(` + Object { + "x-kbn-context": "%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22id%22%3A%2242%22%2C%22description%22%3A%22test-descripton%22%2C%22parent%22%3A%7B%22type%22%3A%22parent-type%22%2C%22name%22%3A%22parent-name%22%2C%22id%22%3A%2241%22%2C%22description%22%3A%22parent-descripton%22%7D%7D", + } + `); + }); + it('trims a string representation of provided execution context if it is bigger max allowed size', () => { const context: KibanaExecutionContext = { type: 'test-type', @@ -65,4 +89,51 @@ describe('KibanaExecutionContext', () => { `); }); }); + describe('toJSON', () => { + it('returns JSON representation of the context', () => { + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context).toJSON(); + expect(value).toEqual(context); + }); + + it('returns JSON representation when the parent context if provided', () => { + const parentAContext: KibanaExecutionContext = { + type: 'parent-a-type', + name: 'parent-a-name', + id: '40', + description: 'parent-a-descripton', + }; + + const parentBContext: KibanaExecutionContext = { + type: 'parent-b-type', + name: 'parent-b-name', + id: '41', + description: 'parent-b-descripton', + parent: parentAContext, + }; + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: parentBContext, + }; + + const value = new ExecutionContextContainer(context).toJSON(); + expect(value).toEqual({ + ...context, + parent: { + ...parentBContext, + parent: parentAContext, + }, + }); + }); + }); }); diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts deleted file mode 100644 index 071e61f17c25..000000000000 --- a/src/core/public/execution_context/execution_context_service.mock.ts +++ /dev/null @@ -1,37 +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 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 type { PublicMethodsOf } from '@kbn/utility-types'; -import type { Plugin } from 'src/core/public'; -import type { ExecutionContextServiceStart } from './execution_context_service'; -import type { ExecutionContextContainer } from './execution_context_container'; - -const createContainerMock = () => { - const mock: jest.Mocked> = { - toHeader: jest.fn(), - toJSON: jest.fn(), - }; - return mock; -}; -const createStartContractMock = () => { - const mock: jest.Mocked = { - create: jest.fn().mockReturnValue(createContainerMock()), - }; - return mock; -}; - -const createMock = (): jest.Mocked => ({ - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), -}); - -export const executionContextServiceMock = { - create: createMock, - createStartContract: createStartContractMock, - createContainer: createContainerMock, -}; diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts deleted file mode 100644 index 934e68d15be0..000000000000 --- a/src/core/public/execution_context/execution_context_service.ts +++ /dev/null @@ -1,39 +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 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 type { CoreService, KibanaExecutionContext } from '../../types'; -import { - ExecutionContextContainer, - IExecutionContextContainer, -} from './execution_context_container'; - -/** - * @public - */ -export interface ExecutionContextServiceStart { - /** - * Creates a context container carrying the meta-data of a runtime operation. - * Provided meta-data will be propagated to Kibana and Elasticsearch servers. - * ```js - * const context = executionContext.create(...); - * http.fetch('/endpoint/', { context }); - * ``` - */ - create: (context: KibanaExecutionContext) => IExecutionContextContainer; -} - -export class ExecutionContextService implements CoreService { - setup() {} - start(): ExecutionContextServiceStart { - return { - create(context: KibanaExecutionContext) { - return new ExecutionContextContainer(context); - }, - }; - } - stop() {} -} diff --git a/src/core/public/execution_context/index.ts b/src/core/public/execution_context/index.ts index d0c8348d864e..b15a967ac714 100644 --- a/src/core/public/execution_context/index.ts +++ b/src/core/public/execution_context/index.ts @@ -7,6 +7,4 @@ */ export type { KibanaExecutionContext } from '../../types'; -export { ExecutionContextService } from './execution_context_service'; -export type { ExecutionContextServiceStart } from './execution_context_service'; -export type { IExecutionContextContainer } from './execution_context_container'; +export { ExecutionContextContainer } from './execution_context_container'; diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index 67ec816d0843..7e3cd9019b08 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -15,7 +15,6 @@ import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; import { HttpResponse, HttpFetchOptionsWithPath } from './types'; -import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; function delay(duration: number) { return new Promise((r) => setTimeout(r, duration)); @@ -230,14 +229,19 @@ describe('Fetch', () => { it('should inject context headers if provided', async () => { fetchMock.get('*', {}); - const executionContainerMock = executionContextServiceMock.createContainer(); - executionContainerMock.toHeader.mockReturnValueOnce({ 'x-kbn-context': 'value' }); + await fetchInstance.fetch('/my/path', { - context: executionContainerMock, + context: { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }, }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'x-kbn-context': 'value', + 'x-kbn-context': + '%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22description%22%3A%22test-description%22%2C%22id%22%3A%2242%22%7D', }); }); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index fb178a937e18..372445b2b090 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -22,6 +22,7 @@ import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptController } from './http_intercept_controller'; import { interceptRequest, interceptResponse } from './intercept'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; +import { ExecutionContextContainer } from '../execution_context'; interface Params { basePath: IBasePath; @@ -124,7 +125,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, - ...options.context?.toHeader(), + ...(options.context ? new ExecutionContextContainer(options.context).toHeader() : {}), }), }; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index ccf68201bc20..3eb718b318f8 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -8,7 +8,7 @@ import { Observable } from 'rxjs'; import { MaybePromise } from '@kbn/utility-types'; -import type { IExecutionContextContainer } from '../execution_context'; +import type { KibanaExecutionContext } from '../execution_context'; /** @public */ export interface HttpSetup { @@ -272,7 +272,7 @@ export interface HttpFetchOptions extends HttpRequestInit { */ asResponse?: boolean; - context?: IExecutionContextContainer; + context?: KibanaExecutionContext; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 40304d27580c..e6e643329187 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,7 +65,6 @@ import { ApplicationSetup, Capabilities, ApplicationStart } from './application' import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { DeprecationsServiceStart } from './deprecations'; -import type { ExecutionContextServiceStart } from './execution_context'; export type { PackageInfo, @@ -186,8 +185,6 @@ export type { export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations'; -export type { IExecutionContextContainer, ExecutionContextServiceStart } from './execution_context'; - export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; export { URL_MAX_LENGTH } from './core_app'; @@ -276,8 +273,6 @@ export interface CoreStart { fatalErrors: FatalErrorsStart; /** {@link DeprecationsServiceStart} */ deprecations: DeprecationsServiceStart; - /** {@link ExecutionContextServiceStart} */ - executionContext: ExecutionContextServiceStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 63b94ea4ac4e..bd7623beba65 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -25,7 +25,6 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; -import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -40,7 +39,6 @@ export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.m export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; -export { executionContextServiceMock } from './execution_context/execution_context_service.mock'; function createCoreSetupMock({ basePath = '', @@ -86,7 +84,6 @@ function createCoreStartMock({ basePath = '' } = {}) { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, fatalErrors: fatalErrorsServiceMock.createStartContract(), - executionContext: executionContextServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index be3cff54aca8..49c895aa80fc 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -140,6 +140,5 @@ export function createPluginStartContext< }, fatalErrors: deps.fatalErrors, deprecations: deps.deprecations, - executionContext: deps.executionContext, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 3f23889c57de..06c72823c775 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -35,7 +35,6 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; -import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; export let mockPluginInitializers: Map; @@ -105,7 +104,6 @@ describe('PluginsService', () => { savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), deprecations: deprecationsServiceMock.createStartContract(), - executionContext: executionContextServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 80fd3927e05a..5ce12a1889c2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -433,8 +433,6 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) - executionContext: ExecutionContextServiceStart; - // (undocumented) fatalErrors: FatalErrorsStart; // (undocumented) http: HttpStart; @@ -717,11 +715,6 @@ export interface ErrorToastOptions extends ToastOptions { toastMessage?: string; } -// @public (undocumented) -export interface ExecutionContextServiceStart { - create: (context: KibanaExecutionContext) => IExecutionContextContainer; -} - // @public export interface FatalErrorInfo { // (undocumented) @@ -761,7 +754,7 @@ export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; asSystemRequest?: boolean; // (undocumented) - context?: IExecutionContextContainer; + context?: KibanaExecutionContext; headers?: HttpHeadersInit; prependBasePath?: boolean; query?: HttpFetchQuery; @@ -897,14 +890,6 @@ export interface IBasePath { readonly serverBasePath: string; } -// @public (undocumented) -export interface IExecutionContextContainer { - // (undocumented) - toHeader: () => Record; - // (undocumented) - toJSON: () => Readonly; -} - // @public export interface IExternalUrl { validateUrl(relativeOrAbsoluteUrl: string): URL | null; @@ -967,14 +952,15 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } -// @public (undocumented) -export interface KibanaExecutionContext { - readonly description: string; - readonly id: string; - readonly name: string; +// @public +export type KibanaExecutionContext = { readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; readonly url?: string; -} + parent?: KibanaExecutionContext; +}; // @public export type MountPoint = (element: T) => UnmountCallback; @@ -1163,9 +1149,9 @@ export type ResolveDeprecationResponse = { // @public export interface ResolvedSimpleSavedObject { - aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; + alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; outcome: SavedObjectsResolveResponse['outcome']; - savedObject: SimpleSavedObject; + saved_object: SimpleSavedObject; } // Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1520,7 +1506,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat // @public (undocumented) export interface SavedObjectsResolveResponse { - aliasTargetId?: string; + alias_target_id?: string; outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; saved_object: SavedObject; } @@ -1692,6 +1678,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:172:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index 85441b9841ea..2eed9615430e 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -155,7 +155,7 @@ describe('SavedObjectsClient', () => { http.fetch.mockResolvedValue({ saved_object: doc, outcome: 'conflict', - aliasTargetId: 'another-id', + alias_target_id: 'another-id', } as SavedObjectsResolveResponse); }); }); @@ -197,11 +197,11 @@ describe('SavedObjectsClient', () => { test('resolves with ResolvedSimpleSavedObject instance', async () => { const result = await savedObjectsClient.resolve(doc.type, doc.id); - expect(result.savedObject).toBeInstanceOf(SimpleSavedObject); - expect(result.savedObject.type).toBe(doc.type); - expect(result.savedObject.get('title')).toBe('Example title'); + expect(result.saved_object).toBeInstanceOf(SimpleSavedObject); + expect(result.saved_object.type).toBe(doc.type); + expect(result.saved_object.get('title')).toBe('Example title'); expect(result.outcome).toBe('conflict'); - expect(result.aliasTargetId).toBe('another-id'); + expect(result.alias_target_id).toBe('another-id'); }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index cb9fca8c8c75..2e4c25035dac 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -441,9 +441,13 @@ export class SavedObjectsClient { const path = `${this.getPath(['resolve'])}/${type}/${id}`; const request: Promise> = this.savedObjectsFetch(path, {}); - return request.then(({ saved_object: object, outcome, aliasTargetId }) => { - const savedObject = new SimpleSavedObject(this, object); - return { savedObject, outcome, aliasTargetId }; + return request.then((resolveResponse) => { + const simpleSavedObject = new SimpleSavedObject(this, resolveResponse.saved_object); + return { + saved_object: simpleSavedObject, + outcome: resolveResponse.outcome, + alias_target_id: resolveResponse.alias_target_id, + }; }); }; diff --git a/src/core/public/saved_objects/types.ts b/src/core/public/saved_objects/types.ts index ac3df1673012..1251e75b5d6e 100644 --- a/src/core/public/saved_objects/types.ts +++ b/src/core/public/saved_objects/types.ts @@ -19,7 +19,7 @@ export interface ResolvedSimpleSavedObject { /** * The saved object that was found. */ - savedObject: SimpleSavedObject; + saved_object: SimpleSavedObject; /** * The outcome for a successful `resolve` call is one of the following values: * @@ -33,5 +33,5 @@ export interface ResolvedSimpleSavedObject { /** * The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. */ - aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; + alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; } diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index d164736cead0..f81b65184301 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -10,7 +10,6 @@ import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http'; import { ensureRawRequest, filterHeaders } from '../../http/router'; -import type { IExecutionContextContainer } from '../../execution_context'; import { ScopeableRequest } from '../types'; import { ElasticsearchClient } from './types'; import { configureClient } from './configure_client'; @@ -64,7 +63,7 @@ export class ClusterClient implements ICustomClusterClient { logger: Logger, type: string, private readonly getAuthHeaders: GetAuthHeaders = noop, - getExecutionContext: () => IExecutionContextContainer | undefined = noop + getExecutionContext: () => string | undefined = noop ) { this.asInternalUser = configureClient(config, { logger, type, getExecutionContext }); this.rootScopedClient = configureClient(config, { diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index f2953862c25e..3c32dd2cfd4f 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -14,7 +14,6 @@ import type { TransportRequestParams, TransportRequestOptions, } from '@elastic/elasticsearch/lib/Transport'; -import type { IExecutionContextContainer } from '../../execution_context'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; @@ -31,14 +30,14 @@ export const configureClient = ( logger: Logger; type: string; scoped?: boolean; - getExecutionContext?: () => IExecutionContextContainer | undefined; + getExecutionContext?: () => string | undefined; } ): Client => { const clientOptions = parseClientOptions(config, scoped); class KibanaTransport extends Transport { request(params: TransportRequestParams, options?: TransportRequestOptions) { const opts = options || {}; - const opaqueId = getExecutionContext()?.toString(); + const opaqueId = getExecutionContext(); if (opaqueId && !opts.opaqueId) { // rewrites headers['x-opaque-id'] if it presents opts.opaqueId = opaqueId; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index a7fbce718022..848d9c204bfb 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -141,9 +141,10 @@ export type MockedTransportRequestPromise = TransportRequestPromise & { const createSuccessTransportRequestPromise = ( body: T, - { statusCode = 200 }: { statusCode?: number } = {} + { statusCode = 200 }: { statusCode?: number } = {}, + headers?: Record ): MockedTransportRequestPromise> => { - const response = createApiResponse({ body, statusCode }); + const response = createApiResponse({ body, statusCode, headers }); const promise = Promise.resolve(response); (promise as MockedTransportRequestPromise>).abort = jest.fn(); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index f983a8b77fe0..acd2204334c0 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -149,7 +149,7 @@ export class ElasticsearchService this.coreContext.logger.get('elasticsearch'), type, this.getAuthHeaders, - () => this.executionContextClient?.get() + () => this.executionContextClient?.getAsHeader() ); } diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index d97e3331c7cf..8bcc841669fc 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -37,3 +37,4 @@ export type { GetResponse, DeleteDocumentResponse, } from './client'; +export { isSupportedEsServer } from './supported_server_response_check'; diff --git a/src/core/server/elasticsearch/supported_server_response_check.ts b/src/core/server/elasticsearch/supported_server_response_check.ts new file mode 100644 index 000000000000..6fe812bc5851 --- /dev/null +++ b/src/core/server/elasticsearch/supported_server_response_check.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 const PRODUCT_RESPONSE_HEADER = 'x-elastic-product'; +/** + * Response headers check to determine if the response is from Elasticsearch + * @param headers Response headers + * @returns boolean + */ +// This check belongs to the elasticsearch service as a dedicated helper method. +export const isSupportedEsServer = (headers: Record | null) => { + return !!headers && headers[PRODUCT_RESPONSE_HEADER] === 'Elasticsearch'; +}; diff --git a/src/core/server/execution_context/execution_context_container.test.ts b/src/core/server/execution_context/execution_context_container.test.ts index e8408d37f40f..41095815a6b4 100644 --- a/src/core/server/execution_context/execution_context_container.test.ts +++ b/src/core/server/execution_context/execution_context_container.test.ts @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { KibanaServerExecutionContext } from './execution_context_service'; +import type { KibanaExecutionContext } from '../../types'; + import { ExecutionContextContainer, getParentContextFrom, @@ -14,52 +15,80 @@ import { } from './execution_context_container'; describe('KibanaExecutionContext', () => { + describe('constructor', () => { + it('allows context to define parent explicitly', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '44', + description: 'parent-descripton', + }; + const parentContainer = new ExecutionContextContainer(parentContext); + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: { + type: 'custom-parent-type', + name: 'custom-parent-name', + id: '41', + description: 'custom-parent-descripton', + }, + }; + + const value = new ExecutionContextContainer(context, parentContainer).toJSON(); + expect(value).toEqual(context); + }); + }); + describe('toString', () => { it('returns a string representation of provided execution context', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { type: 'test-type', name: 'test-name', id: '42', description: 'test-descripton', - requestId: '1234-5678', }; const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot(`"1234-5678;kibana:test-type:test-name:42"`); + expect(value).toBe('test-type:test-name:42'); }); - it('returns a limited representation if optional properties are omitted', () => { - const context: KibanaServerExecutionContext = { - requestId: '1234-5678', + it('includes a parent context to string representation', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', }; + const parentContainer = new ExecutionContextContainer(parentContext); - const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot(`"1234-5678"`); + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context, parentContainer).toString(); + expect(value).toBe('parent-type:parent-name:41;test-type:test-name:42'); }); it('returns an escaped string representation of provided execution contextStringified', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { id: 'Visualization☺漢字', type: 'test-type', name: 'test-name', - requestId: '1234-5678', + description: 'test-description', }; const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot( - `"1234-5678;kibana:test-type:test-name:Visualization%E2%98%BA%E6%BC%A2%E5%AD%97"` - ); + expect(value).toBe('test-type:test-name:Visualization%E2%98%BA%E6%BC%A2%E5%AD%97'); }); it('trims a string representation of provided execution context if it is bigger max allowed size', () => { - expect( - new Blob([ - new ExecutionContextContainer({ - requestId: '1234-5678'.repeat(1000), - }).toString(), - ]).size - ).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS); - expect( new Blob([ new ExecutionContextContainer({ @@ -67,7 +96,6 @@ describe('KibanaExecutionContext', () => { name: 'test-name', id: '42'.repeat(1000), description: 'test-descripton', - requestId: '1234-5678', }).toString(), ]).size ).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS); @@ -76,16 +104,35 @@ describe('KibanaExecutionContext', () => { describe('toJSON', () => { it('returns a context object', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { type: 'test-type', name: 'test-name', id: '42', description: 'test-descripton', - requestId: '1234-5678', }; const value = new ExecutionContextContainer(context).toJSON(); - expect(value).toBe(context); + expect(value).toEqual(context); + }); + + it('returns a context object with registed parent object', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', + }; + const parentContainer = new ExecutionContextContainer(parentContext); + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context, parentContainer).toJSON(); + expect(value).toEqual({ ...context, parent: parentContext }); }); }); }); @@ -97,7 +144,7 @@ describe('getParentContextFrom', () => { expect(getParentContextFrom({ [BAGGAGE_HEADER]: header })).toEqual(ctx); }); - it('does not throw an exception if given not a valid value', () => { + it('does not throw an exception if given not a valid JSON object', () => { expect(getParentContextFrom({ [BAGGAGE_HEADER]: 'value' })).toBeUndefined(); expect(getParentContextFrom({ [BAGGAGE_HEADER]: '' })).toBeUndefined(); expect(getParentContextFrom({})).toBeUndefined(); diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts index 6c4a5606df15..a81c409ab3e9 100644 --- a/src/core/server/execution_context/execution_context_container.ts +++ b/src/core/server/execution_context/execution_context_container.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { KibanaServerExecutionContext } from './execution_context_service'; import type { KibanaExecutionContext } from '../../types'; // Switch to the standard Baggage header. blocked by @@ -47,27 +46,23 @@ function enforceMaxLength(header: string): string { */ export interface IExecutionContextContainer { toString(): string; - toJSON(): Readonly; + toJSON(): Readonly; +} + +function stringify(ctx: KibanaExecutionContext): string { + const stringifiedCtx = `${ctx.type}:${ctx.name}:${encodeURIComponent(ctx.id)}`; + return ctx.parent ? `${stringify(ctx.parent)};${stringifiedCtx}` : stringifiedCtx; } export class ExecutionContextContainer implements IExecutionContextContainer { - readonly #context: Readonly; - constructor(context: Readonly) { - this.#context = context; + readonly #context: Readonly; + constructor(context: KibanaExecutionContext, parent?: IExecutionContextContainer) { + this.#context = { parent: parent?.toJSON(), ...context }; } toString(): string { - const ctx = this.#context; - const contextStringified = - ctx.type && ctx.id && ctx.name - ? // id may contain non-ASCII symbols - `kibana:${encodeURIComponent(ctx.type)}:${encodeURIComponent( - ctx.name - )}:${encodeURIComponent(ctx.id)}` - : ''; - const result = contextStringified ? `${ctx.requestId};${contextStringified}` : ctx.requestId; - return enforceMaxLength(result); + return enforceMaxLength(stringify(this.#context)); } - toJSON(): Readonly { + toJSON() { return this.#context; } } diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts index 657805df273c..2e31145f6c0e 100644 --- a/src/core/server/execution_context/execution_context_service.mock.ts +++ b/src/core/server/execution_context/execution_context_service.mock.ts @@ -15,9 +15,11 @@ import type { const createExecutionContextMock = () => { const mock: jest.Mocked = { set: jest.fn(), - reset: jest.fn(), + setRequestId: jest.fn(), + withContext: jest.fn(), get: jest.fn(), getParentContextFrom: jest.fn(), + getAsHeader: jest.fn(), }; return mock; }; @@ -28,8 +30,7 @@ const createInternalSetupContractMock = () => { const createSetupContractMock = () => { const mock: jest.Mocked = { - set: jest.fn(), - get: jest.fn(), + withContext: jest.fn(), }; return mock; }; diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 0c213429e195..3abaa13d1110 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -25,97 +25,400 @@ describe('ExecutionContextService', () => { service = new ExecutionContextService(core).setup(); }); - it('sets and gets a value in async context', async () => { - const chainA = Promise.resolve().then(async () => { - service.set({ - requestId: '0000', + describe('set', () => { + it('sets and gets a value in async context', async () => { + const chainA = Promise.resolve().then(async () => { + service.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + await delay(500); + return service.get(); + }); + + const chainB = Promise.resolve().then(async () => { + service.set({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }); + await delay(100); + return service.get(); + }); + + expect( + await Promise.all([chainA, chainB]).then((results) => + results.map((result) => result?.toJSON()) + ) + ).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('a sequentual call rewrites the context', async () => { + const result = await Promise.resolve().then(async () => { + service.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + service.set({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }); + + return service.get(); + }); + + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, }); - await delay(500); - return service.get(); }); - const chainB = Promise.resolve().then(async () => { + it('emits context to the logs when "set" is called', async () => { service.set({ - requestId: '1111', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); - }); - - expect( - await Promise.all([chainA, chainB]).then((results) => - results.map((result) => result?.toJSON()) - ) - ).toEqual([ - { - requestId: '0000', - }, - { - requestId: '1111', - }, - ]); + expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "set the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + ], + ] + `); + }); + + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + const chainA = await Promise.resolve().then(async () => { + disabledService.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + await delay(100); + return disabledService.get(); + }); + + expect(chainA).toBeUndefined(); + }); }); - it('sets and resets a value in async context', async () => { - const chainA = Promise.resolve().then(async () => { - service.set({ - requestId: '0000', + describe('withContext', () => { + it('sets and gets a value in async context', async () => { + const chainA = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + const chainB = service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(50); + return service.get(); + } + ); + + expect( + await Promise.all([chainA, chainB]).then((results) => + results.map((result) => result?.toJSON()) + ) + ).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('sets the context for a wrapped function only', () => { + service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + expect(service.get()).toBe(undefined); + }); + + it('a sequentual call does not affect orhers contexts', async () => { + const chainA = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(50); + return service.get(); + } + ); + + const chainB = service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); + const result = await Promise.all([chainA, chainB]); + expect(result.map((r) => r?.toJSON())).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('supports nested contexts', async () => { + const result = await service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + () => service.get() + ); + } + ); + + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, }); - await delay(500); - service.reset(); - return service.get(); }); - const chainB = Promise.resolve().then(async () => { + it('inherits a nested context configured by "set"', async () => { service.set({ - requestId: '1111', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); - }); - - expect( - await Promise.all([chainA, chainB]).then((results) => - results.map((result) => result?.toJSON()) - ) - ).toEqual([ - undefined, - { - requestId: '1111', - }, - ]); - }); + const result = await service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); - it('emits context to the logs when "set" is called', async () => { - service.set({ - requestId: '0000', + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + }); + }); + + it('do not swallow errors', () => { + const error = new Error('oops'); + const promise = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + throw error; + } + ); + + expect(promise).rejects.toBe(error); + }); + + it('emits context to the logs when "withContext" is called', async () => { + service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + (i) => i + ); + expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "stored the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + ], + ] + `); + }); + + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + const result = await disabledService.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + expect(result).toBeUndefined(); }); - expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "stored the execution context: {\\"requestId\\":\\"0000\\"}", - ], - ] - `); }); - }); - describe('config', () => { - it('can be disabled', async () => { - const core = mockCoreContext.create(); - core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: false })); - const service = new ExecutionContextService(core).setup(); - const chainA = await Promise.resolve().then(async () => { + describe('getAsHeader', () => { + it('returns request id if no context provided', async () => { + service.setRequestId('1234'); + + expect(service.getAsHeader()).toBe('1234'); + }); + + it('returns request id and registered context', async () => { + service.setRequestId('1234'); service.set({ - requestId: '0000', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); + + expect(service.getAsHeader()).toBe('1234;kibana:type-a:name-a:id-a'); }); - expect(chainA).toBeUndefined(); + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + disabledService.setRequestId('1234'); + disabledService.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + + expect(disabledService.getAsHeader()).toBeUndefined(); + }); }); + }); + describe('config', () => { it('reacts to config changes', async () => { const core = mockCoreContext.create(); const config$ = new BehaviorSubject({ enabled: false }); @@ -124,7 +427,10 @@ describe('ExecutionContextService', () => { function exec() { return Promise.resolve().then(async () => { service.set({ - requestId: '0000', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); await delay(100); return service.get(); diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index b187283e27e3..5a8d104cebcd 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -19,21 +19,26 @@ import { getParentContextFrom, } from './execution_context_container'; -/** - * @public - */ -export interface KibanaServerExecutionContext extends Partial { - requestId: string; -} - /** * @internal */ export interface IExecutionContext { getParentContextFrom(headers: Record): KibanaExecutionContext | undefined; - set(context: Partial): void; - reset(): void; + setRequestId(requestId: string): void; + set(context: KibanaExecutionContext): void; + /** + * The sole purpose of this imperative internal API is to be used by the http service. + * The event-based nature of Hapi server doesn't allow us to wrap a request handler with "withContext". + * Since all the Hapi event lifecycle will lose the execution context. + * Nodejs docs also recommend using AsyncLocalStorage.run() over AsyncLocalStorage.enterWith(). + * https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_enterwith_store + */ get(): IExecutionContextContainer | undefined; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; + /** + * returns serialized representation to send as a header + **/ + getAsHeader(): string | undefined; } /** @@ -51,15 +56,11 @@ export type InternalExecutionContextStart = IExecutionContext; */ export interface ExecutionContextSetup { /** - * Stores the meta-data of a runtime operation. - * Data are carried over all async operations automatically. - * The sequential calls merge provided "context" object shallowly. - **/ - set(context: Partial): void; - /** - * Retrieves an opearation meta-data for the current async context. + * Keeps track of execution context while the passed function is executed. + * Data are carried over all async operations spawned by the passed function. + * The nested calls stack the registered context on top of each other. **/ - get(): IExecutionContextContainer | undefined; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } /** @@ -70,13 +71,15 @@ export type ExecutionContextStart = ExecutionContextSetup; export class ExecutionContextService implements CoreService { private readonly log: Logger; - private readonly asyncLocalStorage: AsyncLocalStorage; + private readonly contextStore: AsyncLocalStorage; + private readonly requestIdStore: AsyncLocalStorage<{ requestId: string }>; private enabled = false; private configSubscription?: Subscription; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('execution_context'); - this.asyncLocalStorage = new AsyncLocalStorage(); + this.contextStore = new AsyncLocalStorage(); + this.requestIdStore = new AsyncLocalStorage<{ requestId: string }>(); } setup(): InternalExecutionContextSetup { @@ -89,8 +92,10 @@ export class ExecutionContextService return { getParentContextFrom, set: this.set.bind(this), - reset: this.reset.bind(this), + withContext: this.withContext.bind(this), + setRequestId: this.setRequestId.bind(this), get: this.get.bind(this), + getAsHeader: this.getAsHeader.bind(this), }; } @@ -98,8 +103,10 @@ export class ExecutionContextService return { getParentContextFrom, set: this.set.bind(this), - reset: this.reset.bind(this), + setRequestId: this.setRequestId.bind(this), + withContext: this.withContext.bind(this), get: this.get.bind(this), + getAsHeader: this.getAsHeader.bind(this), }; } @@ -111,25 +118,43 @@ export class ExecutionContextService } } - private set(context: KibanaServerExecutionContext) { + private set(context: KibanaExecutionContext) { if (!this.enabled) return; - const prevValue = this.asyncLocalStorage.getStore(); - // merges context objects shallowly. repeats the deafult logic of apm.setCustomContext(ctx) - const contextContainer = new ExecutionContextContainer({ ...prevValue?.toJSON(), ...context }); + const contextContainer = new ExecutionContextContainer(context); // we have to use enterWith since Hapi lifecycle model is built on event emitters. // therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles. - this.asyncLocalStorage.enterWith(contextContainer); + this.contextStore.enterWith(contextContainer); + this.log.debug(`set the execution context: ${JSON.stringify(contextContainer)}`); + } + + private withContext( + context: KibanaExecutionContext | undefined, + fn: (...args: any[]) => R + ): R { + if (!this.enabled || !context) { + return fn(); + } + const parent = this.contextStore.getStore(); + const contextContainer = new ExecutionContextContainer(context, parent); this.log.debug(`stored the execution context: ${JSON.stringify(contextContainer)}`); + + return this.contextStore.run(contextContainer, fn); } - private reset() { + private setRequestId(requestId: string) { if (!this.enabled) return; - // @ts-expect-error "undefined" is not supported in type definitions, which is wrong - this.asyncLocalStorage.enterWith(undefined); + this.requestIdStore.enterWith({ requestId }); } private get(): IExecutionContextContainer | undefined { if (!this.enabled) return; - return this.asyncLocalStorage.getStore(); + return this.contextStore.getStore(); + } + + private getAsHeader(): string | undefined { + if (!this.enabled) return; + const stringifiedCtx = this.contextStore.getStore()?.toString(); + const requestId = this.requestIdStore.getStore()?.requestId; + return stringifiedCtx ? `${requestId};kibana:${stringifiedCtx}` : requestId; } } diff --git a/src/core/server/execution_context/index.ts b/src/core/server/execution_context/index.ts index f8018c75995e..d34eed5cc34f 100644 --- a/src/core/server/execution_context/index.ts +++ b/src/core/server/execution_context/index.ts @@ -14,7 +14,6 @@ export type { ExecutionContextSetup, ExecutionContextStart, IExecutionContext, - KibanaServerExecutionContext, } from './execution_context_service'; export type { IExecutionContextContainer } from './execution_context_container'; export { config } from './execution_context_config'; diff --git a/src/core/server/execution_context/integration_tests/tracing.test.ts b/src/core/server/execution_context/integration_tests/tracing.test.ts index ade67d0dd260..5451ee222d77 100644 --- a/src/core/server/execution_context/integration_tests/tracing.test.ts +++ b/src/core/server/execution_context/integration_tests/tracing.test.ts @@ -205,7 +205,10 @@ describe('trace', () => { executionContext.set(parentContext); const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); return res.ok({ - body: { context: executionContext.get()?.toJSON(), header: headers?.['x-opaque-id'] }, + body: { + context: executionContext.get()?.toJSON(), + header: headers?.['x-opaque-id'], + }, }); }); @@ -237,7 +240,7 @@ describe('trace', () => { await root.start(); const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('sets execution context for an async request handler', async () => { @@ -253,7 +256,7 @@ describe('trace', () => { await root.start(); const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('execution context is uniq for sequential requests', async () => { @@ -261,8 +264,9 @@ describe('trace', () => { const { createRouter } = http; const router = createRouter(''); + let id = 42; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(parentContext); + executionContext.set({ ...parentContext, id: String(id++) }); await delay(100); return res.ok({ body: executionContext.get() }); }); @@ -271,9 +275,8 @@ describe('trace', () => { const responseA = await kbnTestServer.request.get(root, '/execution-context').expect(200); const responseB = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(responseA.body).toEqual({ ...parentContext, requestId: expect.any(String) }); - expect(responseB.body).toEqual({ ...parentContext, requestId: expect.any(String) }); - expect(responseA.body.requestId).not.toBe(responseB.body.requestId); + expect(responseA.body).toEqual({ ...parentContext, id: '42' }); + expect(responseB.body).toEqual({ ...parentContext, id: '43' }); }); it('execution context is uniq for concurrent requests', async () => { @@ -283,7 +286,7 @@ describe('trace', () => { const router = createRouter(''); let id = 2; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(parentContext); + executionContext.set({ ...parentContext, id: String(id) }); await delay(id-- * 100); return res.ok({ body: executionContext.get() }); }); @@ -298,13 +301,10 @@ describe('trace', () => { responseB, responseC, ]); - expect(bodyA.requestId).toBeDefined(); - expect(bodyB.requestId).toBeDefined(); - expect(bodyC.requestId).toBeDefined(); - expect(bodyA.requestId).not.toBe(bodyB.requestId); - expect(bodyB.requestId).not.toBe(bodyC.requestId); - expect(bodyA.requestId).not.toBe(bodyC.requestId); + expect(bodyA.id).toBe('2'); + expect(bodyB.id).toBe('1'); + expect(bodyC.id).toBe('0'); }); it('execution context is uniq for concurrent requests when "x-opaque-id" provided', async () => { @@ -316,7 +316,8 @@ describe('trace', () => { router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { executionContext.set(parentContext); await delay(id-- * 100); - return res.ok({ body: executionContext.get() }); + const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); + return res.ok({ body: headers || {} }); }); await root.start(); @@ -335,9 +336,9 @@ describe('trace', () => { responseB, responseC, ]); - expect(bodyA.requestId).toBe('req-1'); - expect(bodyB.requestId).toBe('req-2'); - expect(bodyC.requestId).toBe('req-3'); + expect(bodyA['x-opaque-id']).toContain('req-1'); + expect(bodyB['x-opaque-id']).toContain('req-2'); + expect(bodyC['x-opaque-id']).toContain('req-3'); }); it('parses the parent context if present', async () => { @@ -355,7 +356,7 @@ describe('trace', () => { .set(new ExecutionContextContainer(parentContext).toHeader()) .expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('execution context is the same for all the lifecycle events', async () => { @@ -410,7 +411,7 @@ describe('trace', () => { .set(new ExecutionContextContainer(parentContext).toHeader()) .expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); expect(response.body).toEqual(onPreRoutingContext); expect(response.body).toEqual(onPreAuthContext); @@ -461,32 +462,26 @@ describe('trace', () => { expect(header).toContain('kibana:test-type:test-name:42'); }); - it('a repeat call overwrites the old context', async () => { - const { http, executionContext } = await root.setup(); + it('passes "x-opaque-id" if no execution context is registered', async () => { + const { http } = await root.setup(); const { createRouter } = http; const router = createRouter(''); - const newContext = { - type: 'new-type', - name: 'new-name', - id: '41', - description: 'new-description', - }; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(newContext); const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); return res.ok({ body: headers || {} }); }); await root.start(); + const myOpaqueId = 'my-opaque-id'; const response = await kbnTestServer.request .get(root, '/execution-context') - .set(new ExecutionContextContainer(parentContext).toHeader()) + .set('x-opaque-id', myOpaqueId) .expect(200); const header = response.body['x-opaque-id']; - expect(header).toContain('kibana:new-type:new-name:41'); + expect(header).toBe(myOpaqueId); }); it('does not affect "x-opaque-id" set by user', async () => { @@ -531,14 +526,83 @@ describe('trace', () => { await root.start(); - const myOpaqueId = 'my-opaque-id'; - const response = await kbnTestServer.request - .get(root, '/execution-context') - .set('x-opaque-id', myOpaqueId) - .expect(200); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); const header = response.body['x-opaque-id']; - expect(header).toBe('my-opaque-id;kibana:test-type:test-name:42'); + expect(header).toContain('kibana:test-type:test-name:42'); + }); + + describe('withContext', () => { + it('sets execution context for a nested function', async () => { + const { executionContext, http } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + return executionContext.withContext(parentContext, () => + res.ok({ body: executionContext.get() }) + ); + }); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); + expect(response.body).toEqual(parentContext); + }); + + it('set execution context inerits a parent if presented', async () => { + const { executionContext, http } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + const nestedContext = { + type: 'nested-type', + name: 'nested-name', + id: '43', + description: 'nested-description', + }; + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + return executionContext.withContext(parentContext, async () => { + await delay(100); + return executionContext.withContext(nestedContext, async () => { + await delay(100); + return res.ok({ body: executionContext.get() }); + }); + }); + }); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); + expect(response.body).toEqual({ ...nestedContext, parent: parentContext }); + }); + + it('extends the execution context passed from the client-side', async () => { + const { http, executionContext } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + const newContext = { + type: 'new-type', + name: 'new-name', + id: '41', + description: 'new-description', + }; + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + const { headers } = await executionContext.withContext(newContext, () => + context.core.elasticsearch.client.asCurrentUser.ping() + ); + return res.ok({ body: headers || {} }); + }); + + await root.start(); + + const response = await kbnTestServer.request + .get(root, '/execution-context') + .set(new ExecutionContextContainer(parentContext).toHeader()) + .expect(200); + + const header = response.body['x-opaque-id']; + expect(header).toContain('kibana:test-type:test-name:42;new-type:new-name:41'); + }); }); }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 85c035154a7a..26725aff71b6 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -337,10 +337,9 @@ export class HttpServer { const requestId = getRequestId(request, config.requestId); const parentContext = executionContext?.getParentContextFrom(request.headers); - executionContext?.set({ - ...parentContext, - requestId, - }); + if (parentContext) executionContext?.set(parentContext); + + executionContext?.setRequestId(requestId); request.app = { ...(request.app ?? {}), diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 191a3bd972f1..6ee95a09de30 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -84,11 +84,7 @@ export type { import type { ExecutionContextSetup, ExecutionContextStart } from './execution_context'; -export type { - IExecutionContextContainer, - KibanaServerExecutionContext, - KibanaExecutionContext, -} from './execution_context'; +export type { IExecutionContextContainer, KibanaExecutionContext } from './execution_context'; export { bootstrap } from './bootstrap'; export type { diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 29194b1e8fc6..b972c6078ca2 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -156,7 +156,9 @@ export function createPluginSetupContext( elasticsearch: { legacy: deps.elasticsearch.legacy, }, - executionContext: deps.executionContext, + executionContext: { + withContext: deps.executionContext.withContext, + }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, registerRouteHandlerContext: < diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 8bda77563be8..f473b3ed0252 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -43,6 +43,8 @@ export const REMOVED_TYPES: string[] = [ 'server', // https://github.com/elastic/kibana/issues/95617 'tsvb-validation-telemetry', + // replaced by osquery-manager-usage-metric + 'osquery-usage-metric', ].sort(); // When migrating from the outdated index we use a read query which excludes diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts index 0bad209ad9ce..ce4c8078e0c9 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts @@ -72,6 +72,7 @@ const previouslyRegisteredTypes = [ 'monitoring-telemetry', 'osquery-saved-query', 'osquery-usage-metric', + 'osquery-manager-usage-metric', 'query', 'sample-data-telemetry', 'search', diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index a366dce626ec..3bea69342925 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -439,4 +439,45 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); + + describe('NotFoundEsUnavailableError', () => { + it('makes an error identifiable as an EsUnavailable error', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); + }); + + it('returns a boom error', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(error).toHaveProperty('isBoom', true); + }); + + it('decorates the error message with the saved object that was not found', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(error.output.payload).toHaveProperty( + 'message', + 'x-elastic-product not present or not recognized: Saved object [foo/bar] not found' + ); + }); + + describe('error.output', () => { + it('specifies the saved object that was not found', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError( + 'foo', + 'bar' + ); + expect(error.output.payload).toHaveProperty( + 'message', + 'x-elastic-product not present or not recognized: Saved object [foo/bar] not found' + ); + }); + + it('sets statusCode to 503', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError( + 'foo', + 'bar' + ); + expect(error.output).toHaveProperty('statusCode', 503); + }); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 581145c7c09d..c1e1e9589b9a 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -202,4 +202,12 @@ export class SavedObjectsErrorHelpers { public static isGeneralError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; } + + public static createGenericNotFoundEsUnavailableError(type: string, id: string) { + const notFoundError = this.createGenericNotFoundError(type, id); + return this.decorateEsUnavailableError( + new Error(`${notFoundError.message}`), + `x-elastic-product not present or not recognized` + ); + } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 78af9f075337..eead42db1ec5 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -44,6 +44,8 @@ const createGenericNotFoundError = (...args) => SavedObjectsErrorHelpers.createGenericNotFoundError(...args).output.payload; const createUnsupportedTypeError = (...args) => SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; +const createGenericNotFoundEsUnavailableError = (...args) => + SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(...args).output.payload; describe('SavedObjectsRepository', () => { let client; @@ -2202,6 +2204,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( @@ -2221,7 +2228,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -2229,12 +2240,30 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); }); + it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + }); + + it(`throws when ES is unable to find the index during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + }); + it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); client.get.mockResolvedValueOnce( @@ -2278,7 +2307,7 @@ describe('SavedObjectsRepository', () => { client.delete.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) ); - await expectNotFoundError(type, id); + await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); }); @@ -2288,7 +2317,7 @@ describe('SavedObjectsRepository', () => { error: { type: 'index_not_found_exception' }, }) ); - await expectNotFoundError(type, id); + await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); }); @@ -3170,7 +3199,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; - + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.get(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.get(type, id, { namespace: ALL_NAMESPACES_STRING }) @@ -3189,7 +3222,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3197,7 +3234,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3213,6 +3254,15 @@ describe('SavedObjectsRepository', () => { }); expect(client.get).toHaveBeenCalledTimes(1); }); + + it(`throws when ES does not return the correct header when finding the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(type, id); + + expect(client.get).toHaveBeenCalledTimes(1); + }); }); describe('returns', () => { @@ -3314,9 +3364,12 @@ describe('SavedObjectsRepository', () => { it('because alias is not used and actual object is not found', async () => { const options = { namespace: undefined }; - const response = { found: false }; client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) // for actual target ); await expectNotFoundError(type, id, options); @@ -3459,7 +3512,7 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual({ saved_object: expect.objectContaining({ type, id: aliasTargetId }), outcome: 'aliasMatch', - aliasTargetId, + alias_target_id: aliasTargetId, }); }; @@ -3501,7 +3554,7 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual({ saved_object: expect.objectContaining({ type, id }), outcome: 'conflict', - aliasTargetId, + alias_target_id: aliasTargetId, }); }); }); @@ -3854,26 +3907,34 @@ describe('SavedObjectsRepository', () => { if (registry.isMultiNamespace(type)) { const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...mockGetResponse }, + { statusCode: 200 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); } client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - // don't need the rest of the source for test purposes, just the namespace and namespaces attributes - get: { - _source: { - namespaces: [options?.namespace ?? 'default'], - namespace: options?.namespace, + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + // don't need the rest of the source for test purposes, just the namespace and namespaces attributes + get: { + _source: { + namespaces: [options?.namespace ?? 'default'], + namespace: options?.namespace, - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, }, }, - }) + { statusCode: 200 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); const result = await savedObjectsRepository.update(type, id, attributes, options); expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); @@ -4059,6 +4120,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.update(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( @@ -4078,7 +4144,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -4086,12 +4156,32 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); }); + it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get with missing Elasticsearch`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); client.get.mockResolvedValueOnce( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6899f8613b07..17b0f10ef67c 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -14,6 +14,7 @@ import { REPOSITORY_RESOLVE_OUTCOME_STATS, } from '../../../core_usage_data'; import type { ElasticsearchClient } from '../../../elasticsearch/'; +import { isSupportedEsServer } from '../../../elasticsearch'; import type { Logger } from '../../../logging'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { @@ -648,7 +649,7 @@ export class SavedObjectsRepository { } } - const { body, statusCode } = await this.client.delete( + const { body, statusCode, headers } = await this.client.delete( { id: rawId, index: this.getIndexForType(type), @@ -665,9 +666,15 @@ export class SavedObjectsRepository { const deleteDocNotFound = body.result === 'not_found'; const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; + const esServerSupported = isSupportedEsServer(headers); if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + if (esServerSupported) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else { + // throw if we can't verify the response is from Elasticsearch + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } } throw new Error( @@ -1009,19 +1016,19 @@ export class SavedObjectsRepository { if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const namespace = normalizeNamespace(options.namespace); - - const { body, statusCode } = await this.client.get( + const { body, statusCode, headers } = await this.client.get( { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), }, { ignore: [404] } ); - const indexNotFound = statusCode === 404; - + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + if (!isFoundGetResponse(body) && !isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } if ( !isFoundGetResponse(body) || indexNotFound || @@ -1030,7 +1037,6 @@ export class SavedObjectsRepository { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return getSavedObjectFromSource(this._registry, type, id, body); } @@ -1140,7 +1146,7 @@ export class SavedObjectsRepository { // @ts-expect-error MultiGetHit._source is optional saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'conflict', - aliasTargetId: legacyUrlAlias.targetId, + alias_target_id: legacyUrlAlias.targetId, }; outcomeStatString = REPOSITORY_RESOLVE_OUTCOME_STATS.CONFLICT; } else if (foundExactMatch) { @@ -1160,7 +1166,7 @@ export class SavedObjectsRepository { aliasMatchDoc ), outcome: 'aliasMatch', - aliasTargetId: legacyUrlAlias.targetId, + alias_target_id: legacyUrlAlias.targetId, }; outcomeStatString = REPOSITORY_RESOLVE_OUTCOME_STATS.ALIAS_MATCH; } @@ -1248,7 +1254,19 @@ export class SavedObjectsRepository { _source_includes: ['namespace', 'namespaces', 'originId'], require_alias: true, }) + .then((res) => { + const indexNotFound = res.statusCode === 404; + const esServerSupported = isSupportedEsServer(res.headers); + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + if (indexNotFound && !esServerSupported) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + return res; + }) .catch((err) => { + if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { + throw err; + } if (SavedObjectsErrorHelpers.isNotFoundError(err)) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -2070,7 +2088,7 @@ export class SavedObjectsRepository { * @param id The ID of the saved object. * @param namespace The target namespace. * @returns Raw document from Elasticsearch. - * @throws Will throw an error if the saved object is not found, or if it doesn't include the target namespace. + * @throws Will throw an error if the saved object is not found, if it doesn't include the target namespace or if the response is not identifiable as an Elasticsearch response. */ private async preflightCheckIncludesNamespace(type: string, id: string, namespace?: string) { if (!this._registry.isMultiNamespace(type)) { @@ -2078,7 +2096,7 @@ export class SavedObjectsRepository { } const rawId = this._serializer.generateRawId(undefined, type, id); - const { body, statusCode } = await this.client.get( + const { body, statusCode, headers } = await this.client.get( { id: rawId, index: this.getIndexForType(type), @@ -2087,6 +2105,14 @@ export class SavedObjectsRepository { ); const indexFound = statusCode !== 404; + + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + const esServerSupported = isSupportedEsServer(headers); + + if (!isFoundGetResponse(body) && !esServerSupported) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + if ( !indexFound || !isFoundGetResponse(body) || diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index abb86d8120a9..00d47d8d1fb0 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -328,7 +328,7 @@ export interface SavedObjectsResolveResponse { /** * The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. */ - aliasTargetId?: string; + alias_target_id?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7f2ce38a5bdd..b18479af23bb 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1048,8 +1048,7 @@ export interface ErrorHttpResponseOptions { // @public (undocumented) export interface ExecutionContextSetup { - get(): IExecutionContextContainer | undefined; - set(context: Partial): void; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } // @public (undocumented) @@ -1236,7 +1235,7 @@ export interface ICustomClusterClient extends IClusterClient { // @public (undocumented) export interface IExecutionContextContainer { // (undocumented) - toJSON(): Readonly; + toJSON(): Readonly; // (undocumented) toString(): string; } @@ -1357,14 +1356,15 @@ export interface IUiSettingsClient { setMany: (changes: Record) => Promise; } -// @public (undocumented) -export interface KibanaExecutionContext { - readonly description: string; - readonly id: string; - readonly name: string; +// @public +export type KibanaExecutionContext = { readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; readonly url?: string; -} + parent?: KibanaExecutionContext; +}; // @public export class KibanaRequest { @@ -1437,12 +1437,6 @@ export const kibanaResponseFactory: { noContent: (options?: HttpResponseOptions) => KibanaResponse; }; -// @public (undocumented) -export interface KibanaServerExecutionContext extends Partial { - // (undocumented) - requestId: string; -} - // Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts // // @public @@ -2523,6 +2517,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; + // (undocumented) static createIndexAliasNotFoundError(alias: string): DecoratedError; // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; @@ -3033,7 +3029,7 @@ export interface SavedObjectsResolveImportErrorsOptions { // @public (undocumented) export interface SavedObjectsResolveResponse { - aliasTargetId?: string; + alias_target_id?: string; outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; saved_object: SavedObject; } diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts index 72f639231996..43a596bd1e0e 100644 --- a/src/core/server/status/routes/status.ts +++ b/src/core/server/status/routes/status.ts @@ -16,6 +16,7 @@ import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '../types'; import { PluginName } from '../../plugins'; import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; import { PackageInfo } from '../../config'; +import { StatusResponse } from '../../../types/status'; const SNAPSHOT_POSTFIX = /-SNAPSHOT$/; @@ -41,55 +42,9 @@ interface StatusInfo { plugins: Record; } -interface StatusHttpBody { - name: string; - uuid: string; - version: { - number: string; - build_hash: string; - build_number: number; - build_snapshot: boolean; - }; +// The moment we remove support for the LegacyStatusInfo, we can use the StatusResponse straight away. +interface StatusHttpBody extends Omit { status: StatusInfo | LegacyStatusInfo; - metrics: { - /** ISO-8601 date string w/o timezone */ - last_updated: string; - collection_interval_in_millis: number; - process: { - memory: { - heap: { - total_in_bytes: number; - used_in_bytes: number; - size_limit: number; - }; - resident_set_size_in_bytes: number; - }; - event_loop_delay: number; - pid: number; - uptime_in_millis: number; - }; - os: { - load: Record; - memory: { - total_in_bytes: number; - used_in_bytes: number; - free_in_bytes: number; - }; - uptime_in_millis: number; - platform: string; - platformRelease: string; - }; - response_times: { - max_in_millis: number; - }; - requests: { - total: number; - disconnects: number; - statusCodes: Record; - status_codes: Record; - }; - concurrent_connections: number; - }; } export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 855962070457..9c042577cfe2 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index df17195d84a9..8a2d657812da 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ -/** @public */ - -export interface KibanaExecutionContext { +/** + * @public + * Represents a meta-information about a Kibana entity initiating a search request. + */ +// use type to make it compatible with SerializableState +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type KibanaExecutionContext = { /** * Kibana application initated an operation. * */ @@ -21,4 +25,6 @@ export interface KibanaExecutionContext { readonly description: string; /** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */ readonly url?: string; -} + /** a context that spawned the current context. */ + parent?: KibanaExecutionContext; +}; diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 97f990f608c0..280310aee8c5 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -15,5 +15,4 @@ export * from './capabilities'; export * from './app_category'; export * from './ui_settings'; export * from './saved_objects'; -export * from './serializable'; export type { KibanaExecutionContext } from './execution_context'; diff --git a/src/core/types/status.ts b/src/core/types/status.ts index 5a59e46ef7d4..58c954fb7005 100644 --- a/src/core/types/status.ts +++ b/src/core/types/status.ts @@ -6,36 +6,58 @@ * Side Public License, v 1. */ -import type { OpsMetrics } from '../server/metrics'; - -export interface ServerStatus { - id: string; - title: string; - state: string; - message: string; - uiColor: string; - icon?: string; - since?: string; +import type { + CoreStatus as CoreStatusFromServer, + ServiceStatus as ServiceStatusFromServer, + ServiceStatusLevel as ServiceStatusLevelFromServer, + OpsMetrics, +} from '../server'; + +/** + * We need this type to convert the object `ServiceStatusLevel` to a union of the possible strings. + * This is because of the "stringification" that occurs when serving HTTP requests. + */ +export type ServiceStatusLevel = ReturnType; + +export interface ServiceStatus extends Omit { + level: ServiceStatusLevel; } -export type ServerMetrics = OpsMetrics & { +/** + * Copy all the services listed in CoreStatus with their specific ServiceStatus declarations + * but overwriting the `level` to its stringified version. + */ +export type CoreStatus = { + [ServiceName in keyof CoreStatusFromServer]: Omit & { + level: ServiceStatusLevel; + }; +}; + +export type ServerMetrics = Omit & { + last_updated: string; collection_interval_in_millis: number; + requests: { + status_codes: Record; + }; }; export interface ServerVersion { number: string; build_hash: string; - build_number: string; - build_snapshot: string; + build_number: number; + build_snapshot: boolean; +} + +export interface StatusInfo { + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; } export interface StatusResponse { name: string; uuid: string; version: ServerVersion; - status: { - overall: ServerStatus; - statuses: ServerStatus[]; - }; + status: StatusInfo; metrics: ServerMetrics; } diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index 8bae63629046..8bc5eb70c943 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -8,10 +8,9 @@ import { resolve } from 'path'; -import { REPO_ROOT } from '@kbn/utils'; +import { REPO_ROOT, kibanaPackageJson } from '@kbn/utils'; import { createAbsolutePathSerializer } from '@kbn/dev-utils'; -import pkg from '../../../../package.json'; import { Config } from './config'; jest.mock('./version_info', () => ({ @@ -36,14 +35,14 @@ const setup = async ({ targetAllPlatforms = true }: { targetAllPlatforms?: boole describe('#getKibanaPkg()', () => { it('returns the parsed package.json from the Kibana repo', async () => { const config = await setup(); - expect(config.getKibanaPkg()).toEqual(pkg); + expect(config.getKibanaPkg()).toEqual(kibanaPackageJson); }); }); describe('#getNodeVersion()', () => { it('returns the node version from the kibana package.json', async () => { const config = await setup(); - expect(config.getNodeVersion()).toEqual(pkg.engines.node); + expect(config.getNodeVersion()).toEqual(kibanaPackageJson.engines.node); }); }); diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index d4b4f98ed295..f9fcbc74b0ef 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -151,6 +151,9 @@ export const CleanExtraFilesFromModules: Task = { '**/.DS_Store', '**/Dockerfile', '**/docker-compose.yml', + + // https://github.com/elastic/kibana/issues/107617 + '**/png-js/images/*.png', ]); log.info( diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index e2d81c5ae175..e65c5542cce7 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -395,7 +395,6 @@ kibana_vars=( xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.enabled - xpack.task_manager.index xpack.task_manager.max_attempts xpack.task_manager.max_poll_inactivity_cycles xpack.task_manager.max_workers diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 97fd74040974..cac02cae20c4 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -10,9 +10,8 @@ import { access, link, unlink, chmod } from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; -import { branch } from '../../../../../../package.json'; import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; import { TemplateContext } from './template_context'; @@ -64,7 +63,7 @@ export async function runDockerGenerator( artifactTarball, imageFlavor, version, - branch, + branch: kibanaPackageJson.branch, license, artifactsDir, imageTag, diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index 26425b7a3e61..db51ff999ccc 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -8,25 +8,31 @@ import Path from 'path'; -import execa from 'execa'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog, REPO_ROOT, ProcRunner } from '@kbn/dev-utils'; -export const REF_CONFIG_PATHS = [Path.resolve(REPO_ROOT, 'tsconfig.refs.json')]; +import { ROOT_REFS_CONFIG_PATH } from './root_refs_config'; -export async function buildAllTsRefs(log: ToolingLog): Promise<{ failed: boolean }> { - for (const path of REF_CONFIG_PATHS) { - const relative = Path.relative(REPO_ROOT, path); - log.debug(`Building TypeScript projects refs for ${relative}...`); - const { failed, stdout } = await execa( - require.resolve('typescript/bin/tsc'), - ['-b', relative, '--pretty'], - { - cwd: REPO_ROOT, - reject: false, - } - ); - log.info(stdout); - if (failed) return { failed }; +export async function buildAllTsRefs({ + log, + procRunner, + verbose, +}: { + log: ToolingLog; + procRunner: ProcRunner; + verbose?: boolean; +}): Promise<{ failed: boolean }> { + const relative = Path.relative(REPO_ROOT, ROOT_REFS_CONFIG_PATH); + log.info(`Building TypeScript projects refs for ${relative}...`); + + try { + await procRunner.run('tsc', { + cmd: Path.relative(REPO_ROOT, require.resolve('typescript/bin/tsc')), + args: ['-b', relative, '--pretty', ...(verbose ? ['--verbose'] : [])], + cwd: REPO_ROOT, + wait: true, + }); + return { failed: false }; + } catch (error) { + return { failed: true }; } - return { failed: false }; } diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index ad7807eb87c6..6b3cb89a7b0e 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -12,8 +12,10 @@ import { run, REPO_ROOT } from '@kbn/dev-utils'; import del from 'del'; import { RefOutputCache } from './ref_output_cache'; -import { buildAllTsRefs, REF_CONFIG_PATHS } from './build_ts_refs'; -import { getOutputsDeep } from './ts_configfile'; +import { buildAllTsRefs } from './build_ts_refs'; +import { updateRootRefsConfig, ROOT_REFS_CONFIG_PATH } from './root_refs_config'; +import { Project } from './project'; +import { PROJECT_CACHE } from './projects'; import { concurrentMap } from './concurrent_map'; const CACHE_WORKING_DIR = Path.resolve(REPO_ROOT, 'data/ts_refs_output_cache'); @@ -28,7 +30,7 @@ const isTypeFailure = (error: any) => export async function runBuildRefsCli() { run( - async ({ log, flags }) => { + async ({ log, flags, procRunner }) => { if (process.env.BUILD_TS_REFS_DISABLE === 'true' && !flags.force) { log.info( 'Building ts refs is disabled because the BUILD_TS_REFS_DISABLE environment variable is set to "true". Pass `--force` to run the build anyway.' @@ -36,7 +38,19 @@ export async function runBuildRefsCli() { return; } - const outDirs = getOutputsDeep(REF_CONFIG_PATHS); + // if the tsconfig.refs.json file is not self-managed then make sure it has + // a reference to every composite project in the repo + await updateRootRefsConfig(log); + + // load all the projects referenced from the root refs config deeply, so we know all + // the ts projects we are going to be cleaning or populating with caches + const projects = Project.load( + ROOT_REFS_CONFIG_PATH, + {}, + { + skipConfigValidation: true, + } + ).getProjectsDeep(PROJECT_CACHE); const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE !== 'false' && !!flags.cache; const doCapture = process.env.BUILD_TS_REFS_CACHE_CAPTURE === 'true'; @@ -44,15 +58,15 @@ export async function runBuildRefsCli() { const doInitCache = cacheEnabled && !doCapture; if (doClean) { - log.info('deleting', outDirs.length, 'ts output directories'); - await concurrentMap(100, outDirs, (outDir) => del(outDir)); + log.info('deleting', projects.outDirs.length, 'ts output directories'); + await concurrentMap(100, projects.outDirs, (outDir) => del(outDir)); } let outputCache; if (cacheEnabled) { outputCache = await RefOutputCache.create({ log, - outDirs, + projects, repoRoot: REPO_ROOT, workingDir: CACHE_WORKING_DIR, upstreamUrl: 'https://github.com/elastic/kibana.git', @@ -64,7 +78,7 @@ export async function runBuildRefsCli() { } try { - await buildAllTsRefs(log); + await buildAllTsRefs({ log, procRunner, verbose: !!flags.verbose }); log.success('ts refs build successfully'); } catch (error) { const typeFailure = isTypeFailure(error); @@ -97,7 +111,7 @@ export async function runBuildRefsCli() { --force Run the build even if the BUILD_TS_REFS_DISABLE is set to "true" --clean Delete outDirs for each ts project before building --no-cache Disable fetching/extracting outDir caches based on the mergeBase with upstream - --ignore-type-failures If tsc reports type errors, ignore them and just log a small warning. + --ignore-type-failures If tsc reports type errors, ignore them and just log a small warning `, }, log: { diff --git a/src/dev/typescript/concurrent_map.ts b/src/dev/typescript/concurrent_map.ts index 793630ab85a5..b40da14f4162 100644 --- a/src/dev/typescript/concurrent_map.ts +++ b/src/dev/typescript/concurrent_map.ts @@ -15,6 +15,10 @@ export async function concurrentMap( arr: T[], fn: (item: T, i: number) => Promise ): Promise { + if (!arr.length) { + return []; + } + return await lastValueFrom( Rx.from(arr).pipe( // execute items in parallel based on concurrency diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/dev/typescript/convert_all_to_composite.ts similarity index 59% rename from src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts rename to src/dev/typescript/convert_all_to_composite.ts index 7cd36d419969..9b9dd3468747 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts +++ b/src/dev/typescript/convert_all_to_composite.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -// @ts-expect-error -import { Pager } from './pager'; +import { run } from '@kbn/dev-utils'; -export function createPagerFactory() { - return { - create(...args: unknown[]) { - return new Pager(...args); - }, - }; -} +import { PROJECTS } from './projects'; + +run(async ({ log }) => { + for (const project of PROJECTS) { + if (!project.config.compilerOptions?.composite) { + log.info(project.tsConfigPath); + } + } +}); diff --git a/src/dev/typescript/exec_in_projects.ts b/src/dev/typescript/exec_in_projects.ts deleted file mode 100644 index ed87aec4ec19..000000000000 --- a/src/dev/typescript/exec_in_projects.ts +++ /dev/null @@ -1,69 +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 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 os from 'os'; - -import { ToolingLog } from '@kbn/dev-utils'; -import chalk from 'chalk'; -import execa from 'execa'; -import Listr from 'listr'; - -import { Project } from './project'; - -class ProjectFailure { - constructor(public project: Project, public error: execa.ExecaError) {} -} - -export function execInProjects( - log: ToolingLog, - projects: Project[], - cmd: string, - getArgs: (project: Project) => string[] -) { - const list = new Listr( - projects.map((project) => ({ - task: () => - execa(cmd, getArgs(project), { - // execute in the current working directory so that relative paths in errors - // are relative from the right location - cwd: process.cwd(), - env: chalk.level > 0 ? { FORCE_COLOR: 'true' } : {}, - stdio: ['ignore', 'pipe', 'pipe'], - preferLocal: true, - }).catch((error) => { - throw new ProjectFailure(project, error); - }), - title: project.name, - })), - { - concurrent: Math.min(4, Math.round((os.cpus() || []).length / 2) || 1) || false, - exitOnError: false, - } - ); - - list.run().catch((error: any) => { - process.exitCode = 1; - - if (!error.errors) { - log.error('Unhandled exception!'); - log.error(error); - process.exit(); - } - - for (const e of error.errors) { - if (e instanceof ProjectFailure) { - log.write(''); - // stdout contains errors from tsc - // stderr conatins tsc crash report - log.error(`${e.project.name} failed\n${e.error.stdout || e.error.stderr}`); - } else { - log.error(e); - } - } - }); -} diff --git a/src/dev/typescript/index.ts b/src/dev/typescript/index.ts index 34ecd76a994d..d9ccc3975b4e 100644 --- a/src/dev/typescript/index.ts +++ b/src/dev/typescript/index.ts @@ -7,8 +7,6 @@ */ export { Project } from './project'; -export { filterProjectsByFlag } from './projects'; export { getTsProjectForAbsolutePath } from './get_ts_project_for_absolute_path'; -export { execInProjects } from './exec_in_projects'; export { runTypeCheckCli } from './run_type_check_cli'; export * from './build_ts_refs_cli'; diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 8d92284e4963..a0c196dd8e91 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { basename, dirname, relative, resolve } from 'path'; +import Path from 'path'; import { IMinimatch, Minimatch } from 'minimatch'; import { REPO_ROOT } from '@kbn/utils'; import { parseTsConfig } from './ts_configfile'; +import { ProjectSet } from './project_set'; function makeMatchers(directory: string, patterns: string[]) { return patterns.map( (pattern) => - new Minimatch(resolve(directory, pattern), { + new Minimatch(Path.resolve(directory, pattern), { dot: true, }) ); @@ -26,41 +27,151 @@ function testMatchers(matchers: IMinimatch[], path: string) { return matchers.some((matcher) => matcher.match(path)); } +export interface ProjectOptions { + name?: string; + disableTypeCheck?: boolean; +} + +interface LoadOptions { + history?: string[]; + cache?: Map; + skipConfigValidation?: boolean; +} + export class Project { - public directory: string; - public name: string; - public config: any; - public disableTypeCheck: boolean; + static load( + tsConfigPath: string, + projectOptions?: ProjectOptions, + loadOptions: LoadOptions = {} + ): Project { + const cache = loadOptions.cache ?? new Map(); + const cached = cache.get(tsConfigPath); + if (cached) { + return cached; + } - private readonly include: IMinimatch[]; - private readonly exclude: IMinimatch[]; + const config = parseTsConfig(tsConfigPath); - constructor( - public tsConfigPath: string, - options: { name?: string; disableTypeCheck?: boolean } = {} - ) { - this.config = parseTsConfig(tsConfigPath); - - const { files, include, exclude = [] } = this.config as { - files?: string[]; - include?: string[]; - exclude?: string[]; - }; - - if (files || !include) { - throw new Error( - 'tsconfig.json files in the Kibana repo must use "include" keys and not "files"' + if (!loadOptions?.skipConfigValidation) { + if (config.files) { + throw new Error(`${tsConfigPath} must not use "files" key`); + } + + if (!config.include) { + throw new Error(`${tsConfigPath} must have an "include" key`); + } + } + + const directory = Path.dirname(tsConfigPath); + const disableTypeCheck = projectOptions?.disableTypeCheck || false; + const name = + projectOptions?.name || Path.relative(REPO_ROOT, directory) || Path.basename(directory); + const include = config.include ? makeMatchers(directory, config.include) : undefined; + const exclude = config.exclude ? makeMatchers(directory, config.exclude) : undefined; + + let baseProject; + if (config.extends) { + const baseConfigPath = Path.resolve(directory, config.extends); + + // prevent circular deps + if (loadOptions.history?.includes(baseConfigPath)) { + throw new Error( + `circular "extends" are not supported in tsconfig files: ${loadOptions.history} => ${baseConfigPath}` + ); + } + + baseProject = Project.load( + baseConfigPath, + {}, + { + skipConfigValidation: true, + history: [...(loadOptions.history ?? []), tsConfigPath], + cache, + } ); } - this.directory = dirname(this.tsConfigPath); - this.disableTypeCheck = options.disableTypeCheck || false; - this.name = options.name || relative(REPO_ROOT, this.directory) || basename(this.directory); - this.include = makeMatchers(this.directory, include); - this.exclude = makeMatchers(this.directory, exclude); + const project = new Project( + tsConfigPath, + directory, + name, + config, + disableTypeCheck, + baseProject, + include, + exclude + ); + cache.set(tsConfigPath, project); + return project; + } + + constructor( + public readonly tsConfigPath: string, + public readonly directory: string, + public readonly name: string, + public readonly config: any, + public readonly disableTypeCheck: boolean, + + public readonly baseProject?: Project, + private readonly include?: IMinimatch[], + private readonly exclude?: IMinimatch[] + ) {} + + private getInclude(): IMinimatch[] { + return this.include ? this.include : this.baseProject?.getInclude() ?? []; + } + + private getExclude(): IMinimatch[] { + return this.exclude ? this.exclude : this.baseProject?.getExclude() ?? []; } public isAbsolutePathSelected(path: string) { - return testMatchers(this.exclude, path) ? false : testMatchers(this.include, path); + return testMatchers(this.getExclude(), path) ? false : testMatchers(this.getInclude(), path); + } + + public isCompositeProject(): boolean { + const own = this.config.compilerOptions?.composite; + return !!(own === undefined ? this.baseProject?.isCompositeProject() : own); + } + + public getOutDir(): string | undefined { + if (this.config.compilerOptions?.outDir) { + return Path.resolve(this.directory, this.config.compilerOptions.outDir); + } + if (this.baseProject) { + return this.baseProject.getOutDir(); + } + return undefined; + } + + public getRefdPaths(): string[] { + if (this.config.references) { + return (this.config.references as Array<{ path: string }>).map(({ path }) => + Path.resolve(this.directory, path) + ); + } + + return this.baseProject ? this.baseProject.getRefdPaths() : []; + } + + public getProjectsDeep(cache?: Map) { + const projects = new Set(); + const queue = new Set([this.tsConfigPath]); + + for (const path of queue) { + const project = Project.load(path, {}, { skipConfigValidation: true, cache }); + projects.add(project); + for (const refPath of project.getRefdPaths()) { + queue.add(refPath); + } + } + + return new ProjectSet(projects); + } + + public getConfigPaths(): string[] { + return this.baseProject + ? [this.tsConfigPath, ...this.baseProject.getConfigPaths()] + : [this.tsConfigPath]; } } diff --git a/src/dev/typescript/project_set.ts b/src/dev/typescript/project_set.ts new file mode 100644 index 000000000000..4ef3693cf6d0 --- /dev/null +++ b/src/dev/typescript/project_set.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Project } from './project'; + +export class ProjectSet { + public readonly outDirs: string[]; + private readonly projects: Project[]; + + constructor(projects: Iterable) { + this.projects = [...projects]; + this.outDirs = this.projects + .map((p) => p.getOutDir()) + .filter((p): p is string => typeof p === 'string'); + } + + filterByPaths(paths: string[]) { + return new ProjectSet( + this.projects.filter((p) => + paths.some((f) => p.isAbsolutePathSelected(f) || p.getConfigPaths().includes(f)) + ) + ); + } +} diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 2c54bb8dba17..419d4f0854ec 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -7,67 +7,57 @@ */ import glob from 'glob'; -import { resolve } from 'path'; +import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { Project } from './project'; +import { Project, ProjectOptions } from './project'; + +/** + * Simple map of all projects defined in this file, to speed of some other operations + * which need to load files by path and to avoid re-parsing base config files hundreds of times + */ +export const PROJECT_CACHE = new Map(); + +const createProject = (rootRelativePath: string, options: ProjectOptions = {}) => + Project.load(Path.resolve(REPO_ROOT, rootRelativePath), options, { + cache: PROJECT_CACHE, + }); + +const findProjects = (pattern: string) => + // NOTE: using glob.sync rather than glob-all or globby + // because it takes less than 10 ms, while the other modules + // both took closer to 1000ms. + glob.sync(pattern, { cwd: REPO_ROOT }).map((path) => createProject(path)); export const PROJECTS = [ - new Project(resolve(REPO_ROOT, 'tsconfig.json')), - new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), - new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), - new Project(resolve(REPO_ROOT, 'src/core/tsconfig.json')), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/drilldowns/url_drilldown/tsconfig.json'), { + createProject('tsconfig.json'), + createProject('test/tsconfig.json', { name: 'kibana/test' }), + createProject('x-pack/test/tsconfig.json', { name: 'x-pack/test' }), + createProject('src/core/tsconfig.json'), + + createProject('x-pack/plugins/drilldowns/url_drilldown/tsconfig.json', { name: 'security_solution/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { + createProject('x-pack/plugins/security_solution/cypress/tsconfig.json', { name: 'security_solution/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), { + createProject('x-pack/plugins/osquery/cypress/tsconfig.json', { name: 'osquery/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { + createProject('x-pack/plugins/apm/e2e/tsconfig.json', { name: 'apm/cypress', disableTypeCheck: true, }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/ftr_e2e/tsconfig.json'), { + createProject('x-pack/plugins/apm/ftr_e2e/tsconfig.json', { name: 'apm/ftr_e2e', disableTypeCheck: true, }), - // NOTE: using glob.sync rather than glob-all or globby - // because it takes less than 10 ms, while the other modules - // both took closer to 1000ms. - ...glob - .sync('packages/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('src/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('x-pack/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('examples/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('x-pack/examples/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), + ...findProjects('packages/*/tsconfig.json'), + ...findProjects('src/plugins/*/tsconfig.json'), + ...findProjects('x-pack/plugins/*/tsconfig.json'), + ...findProjects('examples/*/tsconfig.json'), + ...findProjects('x-pack/examples/*/tsconfig.json'), + ...findProjects('test/plugin_functional/plugins/*/tsconfig.json'), + ...findProjects('test/interpreter_functional/plugins/*/tsconfig.json'), + ...findProjects('test/server_integration/__fixtures__/plugins/*/tsconfig.json'), ]; - -export function filterProjectsByFlag(projectFlag?: string) { - if (!projectFlag) { - return PROJECTS; - } - - const tsConfigPath = resolve(projectFlag); - return PROJECTS.filter((project) => project.tsConfigPath === tsConfigPath); -} diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts index 734752923917..370ef9d17a04 100644 --- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts +++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts @@ -23,6 +23,8 @@ import { import { RefOutputCache, OUTDIR_MERGE_BASE_FILENAME } from '../ref_output_cache'; import { Archives } from '../archives'; import type { RepoInfo } from '../repo_info'; +import { Project } from '../../project'; +import { ProjectSet } from '../../project_set'; jest.mock('../repo_info'); const { RepoInfo: MockRepoInfo } = jest.requireMock('../repo_info'); @@ -48,25 +50,41 @@ afterEach(async () => { logWriter.messages.length = 0; }); -it('creates and extracts caches, ingoring dirs with matching merge-base file and placing merge-base files', async () => { +function makeMockProject(path: string) { + Fs.mkdirSync(Path.resolve(path, 'target/test-types'), { recursive: true }); + Fs.writeFileSync( + Path.resolve(path, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + outDir: './target/test-types', + }, + include: ['**/*'], + exclude: ['test/target/**/*'], + }) + ); + + return Project.load(Path.resolve(path, 'tsconfig.json')); +} + +it('creates and extracts caches, ingoring dirs with matching merge-base file, placing merge-base files, and overriding modified time for updated inputs', async () => { // setup repo mock const HEAD = 'abcdefg'; repo.getHeadSha.mockResolvedValue(HEAD); repo.getRelative.mockImplementation((path) => Path.relative(TMP, path)); repo.getRecentShasFrom.mockResolvedValue(['5678', '1234']); + repo.getFilesChangesSinceSha.mockResolvedValue([]); // create two fake outDirs - const outDirs = [Path.resolve(TMP, 'out/foo'), Path.resolve(TMP, 'out/bar')]; - for (const dir of outDirs) { - Fs.mkdirSync(dir, { recursive: true }); - Fs.writeFileSync(Path.resolve(dir, 'test'), 'hello world'); - } + const projects = new ProjectSet([ + makeMockProject(Path.resolve(TMP, 'test1')), + makeMockProject(Path.resolve(TMP, 'test2')), + ]); // init an archives instance using tmp const archives = await Archives.create(log, TMP); // init the RefOutputCache with our mock data - const refOutputCache = new RefOutputCache(log, repo, archives, outDirs, HEAD); + const refOutputCache = new RefOutputCache(log, repo, archives, projects, HEAD); // create the new cache right in the archives dir await refOutputCache.captureCache(Path.resolve(TMP)); @@ -88,19 +106,21 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and }); // modify the files in the outDirs so we can see which ones are restored from the cache - for (const dir of outDirs) { - Fs.writeFileSync(Path.resolve(dir, 'test'), 'not cleared by cache init'); + for (const dir of projects.outDirs) { + Fs.writeFileSync(Path.resolve(dir, 'no-cleared.txt'), 'not cleared by cache init'); } - // add the mergeBase to the first outDir so that it is ignored - Fs.writeFileSync(Path.resolve(outDirs[0], OUTDIR_MERGE_BASE_FILENAME), HEAD); + + // add the mergeBase to test1 outDir so that it is not cleared + Fs.writeFileSync(Path.resolve(projects.outDirs[0], OUTDIR_MERGE_BASE_FILENAME), HEAD); // rebuild the outDir from the refOutputCache await refOutputCache.initCaches(); + // verify that "test1" outdir is untouched and that "test2" is cleared out const files = Object.fromEntries( globby .sync( - outDirs.map((p) => normalize(p)), + projects.outDirs.map((p) => normalize(p)), { dot: true } ) .map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')]) @@ -108,10 +128,9 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and expect(files).toMatchInlineSnapshot(` Object { - "out/bar/.ts-ref-cache-merge-base": "abcdefg", - "out/bar/test": "hello world", - "out/foo/.ts-ref-cache-merge-base": "abcdefg", - "out/foo/test": "not cleared by cache init", + "test1/target/test-types/.ts-ref-cache-merge-base": "abcdefg", + "test1/target/test-types/no-cleared.txt": "not cleared by cache init", + "test2/target/test-types/.ts-ref-cache-merge-base": "abcdefg", } `); expect(logWriter.messages).toMatchInlineSnapshot(` @@ -124,7 +143,7 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and " debg download complete, renaming tmp", " debg download of cache for abcdefg complete", " debg extracting archives/abcdefg.zip to rebuild caches in 1 outDirs", - " debg [out/bar] clearing outDir and replacing with cache", + " debg [test2/target/test-types] clearing outDir and replacing with cache", ] `); }); @@ -138,7 +157,7 @@ it('cleans up oldest archives when there are more than 10', async () => { } const archives = await Archives.create(log, TMP); - const cache = new RefOutputCache(log, repo, archives, [], '1234'); + const cache = new RefOutputCache(log, repo, archives, new ProjectSet([]), '1234'); expect(cache.archives.size()).toBe(102); await cache.cleanup(); expect(cache.archives.size()).toBe(10); diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index ca69236a706d..b7e641ceb33d 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -9,14 +9,15 @@ import Path from 'path'; import Fs from 'fs/promises'; -import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; +import { ToolingLog, kibanaPackageJson, extract } from '@kbn/dev-utils'; import del from 'del'; import tempy from 'tempy'; import { Archives } from './archives'; -import { unzip, zip } from './zip'; +import { zip } from './zip'; import { concurrentMap } from '../concurrent_map'; import { RepoInfo } from './repo_info'; +import { ProjectSet } from '../project_set'; export const OUTDIR_MERGE_BASE_FILENAME = '.ts-ref-cache-merge-base'; @@ -49,7 +50,7 @@ export class RefOutputCache { static async create(options: { log: ToolingLog; workingDir: string; - outDirs: string[]; + projects: ProjectSet; repoRoot: string; upstreamUrl: string; }) { @@ -59,14 +60,14 @@ export class RefOutputCache { const upstreamBranch: string = kibanaPackageJson.branch; const mergeBase = await repoInfo.getMergeBase('HEAD', upstreamBranch); - return new RefOutputCache(options.log, repoInfo, archives, options.outDirs, mergeBase); + return new RefOutputCache(options.log, repoInfo, archives, options.projects, mergeBase); } constructor( private readonly log: ToolingLog, private readonly repo: RepoInfo, public readonly archives: Archives, - private readonly outDirs: string[], + private readonly projects: ProjectSet, private readonly mergeBase: string ) {} @@ -77,7 +78,7 @@ export class RefOutputCache { */ async initCaches() { const outdatedOutDirs = ( - await concurrentMap(100, this.outDirs, async (outDir) => ({ + await concurrentMap(100, this.projects.outDirs, async (outDir) => ({ path: outDir, outdated: !(await matchMergeBase(outDir, this.mergeBase)), })) @@ -101,6 +102,9 @@ export class RefOutputCache { return; } + const changedFiles = await this.repo.getFilesChangesSinceSha(archive.sha); + const outDirsForcingExtraCacheCheck = this.projects.filterByPaths(changedFiles).outDirs; + const tmpDir = tempy.directory(); this.log.debug( 'extracting', @@ -109,9 +113,13 @@ export class RefOutputCache { outdatedOutDirs.length, 'outDirs' ); - await unzip(archive.path, tmpDir); + await extract({ + archivePath: archive.path, + targetDir: tmpDir, + }); const cacheNames = await Fs.readdir(tmpDir); + const beginningOfTime = new Date(0); await concurrentMap(50, outdatedOutDirs, async (outDir) => { const relative = this.repo.getRelative(outDir); @@ -129,9 +137,22 @@ export class RefOutputCache { return; } - this.log.debug(`[${relative}] clearing outDir and replacing with cache`); + const setModifiedTimes = outDirsForcingExtraCacheCheck.includes(outDir) + ? beginningOfTime + : undefined; + + if (setModifiedTimes) { + this.log.debug(`[${relative}] replacing outDir with cache (forcing revalidation)`); + } else { + this.log.debug(`[${relative}] clearing outDir and replacing with cache`); + } + await del(outDir); - await unzip(Path.resolve(tmpDir, cacheName), outDir); + await extract({ + archivePath: Path.resolve(tmpDir, cacheName), + targetDir: outDir, + setModifiedTimes, + }); await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), this.mergeBase); }); } @@ -154,7 +175,7 @@ export class RefOutputCache { const subZips: Array<[string, string]> = []; await Promise.all( - this.outDirs.map(async (absolute) => { + this.projects.outDirs.map(async (absolute) => { const relative = this.repo.getRelative(absolute); const subZipName = `${relative.split(Path.sep).join('__')}.zip`; const subZipPath = Path.resolve(tmpDir, subZipName); diff --git a/src/dev/typescript/ref_output_cache/repo_info.ts b/src/dev/typescript/ref_output_cache/repo_info.ts index 9a51f3f75182..0bba010bc185 100644 --- a/src/dev/typescript/ref_output_cache/repo_info.ts +++ b/src/dev/typescript/ref_output_cache/repo_info.ts @@ -52,4 +52,20 @@ export class RepoInfo { return proc.stdout.trim(); } + + async getFilesChangesSinceSha(sha: string) { + this.log.debug('determining files changes since sha', sha); + + const proc = await execa('git', ['diff', '--name-only', sha], { + cwd: this.dir, + }); + const files = proc.stdout + .trim() + .split('\n') + .map((p) => Path.resolve(this.dir, p)); + + this.log.verbose('found the following changes compared to', sha, `\n - ${files.join('\n - ')}`); + + return files; + } } diff --git a/src/dev/typescript/ref_output_cache/zip.ts b/src/dev/typescript/ref_output_cache/zip.ts index b1bd8f514bb9..6b0ee053367d 100644 --- a/src/dev/typescript/ref_output_cache/zip.ts +++ b/src/dev/typescript/ref_output_cache/zip.ts @@ -12,7 +12,6 @@ import Path from 'path'; import { pipeline } from 'stream'; import { promisify } from 'util'; -import extractZip from 'extract-zip'; import archiver from 'archiver'; const asyncPipeline = promisify(pipeline); @@ -44,9 +43,3 @@ export async function zip( // await the promise from the pipeline and archive.finalize() await Promise.all([asyncPipeline(archive, createWriteStream(outputPath)), archive.finalize()]); } - -export async function unzip(path: string, outputDir: string) { - await extractZip(path, { - dir: outputDir, - }); -} diff --git a/src/dev/typescript/root_refs_config.ts b/src/dev/typescript/root_refs_config.ts new file mode 100644 index 000000000000..f4aa88f1ea6b --- /dev/null +++ b/src/dev/typescript/root_refs_config.ts @@ -0,0 +1,62 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs/promises'; + +import dedent from 'dedent'; +import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import normalize from 'normalize-path'; + +import { PROJECTS } from './projects'; + +export const ROOT_REFS_CONFIG_PATH = Path.resolve(REPO_ROOT, 'tsconfig.refs.json'); +export const REF_CONFIG_PATHS = [ROOT_REFS_CONFIG_PATH]; + +async function isRootRefsConfigSelfManaged() { + try { + const currentRefsFile = await Fs.readFile(ROOT_REFS_CONFIG_PATH, 'utf-8'); + return currentRefsFile.trim().startsWith('// SELF MANAGED'); + } catch (error) { + if (error.code === 'ENOENT') { + return false; + } + + throw error; + } +} + +function generateTsConfig(refs: string[]) { + return dedent` + // This file is automatically updated when you run \`node scripts/build_ts_refs\`. If you start this + // file with the text "// SELF MANAGED" then you can comment out any projects that you like and only + // build types for specific projects and their dependencies + { + "include": [], + "references": [ +${refs.map((p) => ` { "path": ${JSON.stringify(p)} },`).join('\n')} + ] + } + `; +} + +export async function updateRootRefsConfig(log: ToolingLog) { + if (await isRootRefsConfigSelfManaged()) { + log.warning( + 'tsconfig.refs.json starts with "// SELF MANAGED" so not updating to include all projects' + ); + return; + } + + const refs = PROJECTS.filter((p) => p.isCompositeProject()) + .map((p) => `./${normalize(Path.relative(REPO_ROOT, p.tsConfigPath))}`) + .sort((a, b) => a.localeCompare(b)); + + log.debug('updating', ROOT_REFS_CONFIG_PATH); + await Fs.writeFile(ROOT_REFS_CONFIG_PATH, generateTsConfig(refs) + '\n'); +} diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index d9e9eb036fe0..6a2863132285 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -6,96 +6,131 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; -import chalk from 'chalk'; -import dedent from 'dedent'; -import getopts from 'getopts'; +import Path from 'path'; +import Os from 'os'; -import { execInProjects } from './exec_in_projects'; -import { filterProjectsByFlag } from './projects'; +import * as Rx from 'rxjs'; +import { mergeMap, reduce } from 'rxjs/operators'; +import execa from 'execa'; +import { run, createFailError } from '@kbn/dev-utils'; +import { lastValueFrom } from '@kbn/std'; + +import { PROJECTS } from './projects'; import { buildAllTsRefs } from './build_ts_refs'; +import { updateRootRefsConfig } from './root_refs_config'; export async function runTypeCheckCli() { - const extraFlags: string[] = []; - const opts = getopts(process.argv.slice(2), { - boolean: ['skip-lib-check', 'help'], - default: { - project: undefined, - }, - unknown(name) { - extraFlags.push(name); - return false; + run( + async ({ log, flags, procRunner }) => { + // if the tsconfig.refs.json file is not self-managed then make sure it has + // a reference to every composite project in the repo + await updateRootRefsConfig(log); + + const { failed } = await buildAllTsRefs({ log, procRunner, verbose: !!flags.verbose }); + if (failed) { + throw createFailError('Unable to build TS project refs'); + } + + const projectFilter = + flags.project && typeof flags.project === 'string' + ? Path.resolve(flags.project) + : undefined; + + const projects = PROJECTS.filter((p) => { + return !p.disableTypeCheck && (!projectFilter || p.tsConfigPath === projectFilter); + }); + + if (!projects.length) { + if (projectFilter) { + throw createFailError(`Unable to find project at ${flags.project}`); + } else { + throw createFailError(`Unable to find projects to type-check`); + } + } + + const nonCompositeProjects = projects.filter((p) => !p.isCompositeProject()); + if (!nonCompositeProjects.length) { + if (projectFilter) { + log.success( + `${flags.project} is a composite project so its types are validated by scripts/build_ts_refs` + ); + } else { + log.success( + `All projects are composite so their types are validated by scripts/build_ts_refs` + ); + } + + return; + } + + const concurrency = Math.min(4, Math.round((Os.cpus() || []).length / 2) || 1) || 1; + log.info('running type check in', nonCompositeProjects.length, 'non-composite projects'); + + const tscArgs = [ + ...['--emitDeclarationOnly', 'false'], + '--noEmit', + '--pretty', + ...(flags['skip-lib-check'] + ? ['--skipLibCheck', flags['skip-lib-check'] as string] + : ['--skipLibCheck', 'false']), + ]; + + const failureCount = await lastValueFrom( + Rx.from(nonCompositeProjects).pipe( + mergeMap(async (p) => { + const relativePath = Path.relative(process.cwd(), p.tsConfigPath); + + const result = await execa( + process.execPath, + [ + '--max-old-space-size=5120', + require.resolve('typescript/bin/tsc'), + ...['--project', p.tsConfigPath, ...(flags.verbose ? ['--verbose'] : [])], + ...tscArgs, + ], + { + reject: false, + all: true, + } + ); + + if (result.failed) { + log.error(`Type check failed in ${relativePath}:`); + log.error(result.all ?? ' - tsc produced no output - '); + return 1; + } else { + log.success(relativePath); + return 0; + } + }, concurrency), + reduce((acc, f) => acc + f, 0) + ) + ); + + if (failureCount > 0) { + throw createFailError(`${failureCount} type checks failed`); + } }, - }); - - const log = new ToolingLog({ - level: 'info', - writeTo: process.stdout, - }); - - if (extraFlags.length) { - for (const flag of extraFlags) { - log.error(`Unknown flag: ${flag}`); - } - - process.exitCode = 1; - opts.help = true; - } - - if (opts.help) { - process.stdout.write( - dedent(chalk` - {dim usage:} node scripts/type_check [...options] - - Run the TypeScript compiler without emitting files so that it can check - types during development. + { + description: ` + Run the TypeScript compiler without emitting files so that it can check types during development. Examples: - - {dim # check types in all projects} - {dim $} node scripts/type_check - - {dim # check types in a single project} - {dim $} node scripts/type_check --project packages/kbn-pm/tsconfig.json - - Options: - - --project [path] {dim Path to a tsconfig.json file determines the project to check} - --skip-lib-check {dim Skip type checking of all declaration files (*.d.ts). Default is false} - --help {dim Show this message} - `) - ); - process.stdout.write('\n'); - process.exit(); - } - - const { failed } = await buildAllTsRefs(log); - if (failed) { - log.error('Unable to build TS project refs'); - process.exit(1); - } - - const tscArgs = [ - // composite project cannot be used with --noEmit - ...['--composite', 'false'], - ...['--emitDeclarationOnly', 'false'], - '--noEmit', - '--pretty', - ...(opts['skip-lib-check'] - ? ['--skipLibCheck', opts['skip-lib-check']] - : ['--skipLibCheck', 'false']), - ]; - const projects = filterProjectsByFlag(opts.project).filter((p) => !p.disableTypeCheck); - - if (!projects.length) { - log.error(`Unable to find project at ${opts.project}`); - process.exit(1); - } - - execInProjects(log, projects, process.execPath, (project) => [ - '--max-old-space-size=5120', - require.resolve('typescript/bin/tsc'), - ...['--project', project.tsConfigPath], - ...tscArgs, - ]); + # check types in all projects + node scripts/type_check + + # check types in a single project + node scripts/type_check --project packages/kbn-pm/tsconfig.json + `, + flags: { + string: ['project'], + boolean: ['skip-lib-check'], + help: ` + --project [path] Path to a tsconfig.json file determines the project to check + --skip-lib-check Skip type checking of all declaration files (*.d.ts). Default is false + --help Show this message + `, + }, + } + ); } diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 4d62e410326b..b207f600cbd4 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json index aeb6837c69a9..08ed86d5da0a 100644 --- a/src/plugins/apm_oss/tsconfig.json +++ b/src/plugins/apm_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json index 173ff725d07d..8fceb7d815ee 100644 --- a/src/plugins/bfetch/tsconfig.json +++ b/src/plugins/bfetch/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json index a4f65d593720..fc05a2606865 100644 --- a/src/plugins/charts/tsconfig.json +++ b/src/plugins/charts/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index 34aca5021bac..ee6fbfebc77a 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index b6e2a7c4b8f0..953bd619c444 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -11,7 +11,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import uuid from 'uuid'; -import { CoreStart, IUiSettingsClient } from 'src/core/public'; +import { CoreStart, IUiSettingsClient, KibanaExecutionContext } from 'src/core/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { UiActionsStart } from '../../services/ui_actions'; @@ -68,6 +68,7 @@ export interface InheritedChildInput extends IndexSignature { id: string; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext; } export type DashboardReactContextValue = KibanaReactContextValue; @@ -249,6 +250,7 @@ export class DashboardContainer extends Container { const { search: { session }, @@ -102,6 +105,7 @@ export const buildDashboardContainer = async ({ query: data.query, searchSessionId, savedDashboard, + executionContext, }); /** diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts index ee2ec2bb14fe..2e6290ec920c 100644 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts @@ -7,6 +7,7 @@ */ import _ from 'lodash'; +import type { KibanaExecutionContext } from 'src/core/public'; import { DashboardSavedObject } from '../../saved_dashboards'; import { getTagsFromSavedDashboard, migrateAppState } from '.'; import { EmbeddablePackageState, ViewMode } from '../../services/embeddable'; @@ -40,6 +41,7 @@ interface StateToDashboardContainerInputProps { query: DashboardBuildContext['query']; incomingEmbeddable?: EmbeddablePackageState; dashboardCapabilities: DashboardBuildContext['dashboardCapabilities']; + executionContext?: KibanaExecutionContext; } interface StateToRawDashboardStateProps { @@ -92,6 +94,7 @@ export const stateToDashboardContainerInput = ({ searchSessionId, savedDashboard, dashboardState, + executionContext, }: StateToDashboardContainerInputProps): DashboardContainerInput => { const { filterManager, timefilter: timefilterService } = queryService; const { timefilter } = timefilterService; @@ -125,6 +128,7 @@ export const stateToDashboardContainerInput = ({ timeRange: { ..._.cloneDeep(timefilter.getTime()), }, + executionContext, }; }; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index b1abba44891f..8f0c8acf8102 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -29,7 +29,7 @@ export const diffDashboardContainerInput = ( return commonDiffFilters( (originalInput as unknown) as DashboardDiffCommonFilters, (newInput as unknown) as DashboardDiffCommonFilters, - ['searchSessionId', 'lastReloadRequestTime'] + ['searchSessionId', 'lastReloadRequestTime', 'executionContext'] ); }; diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index e154351819ee..ed4e7a5dd4d4 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; @@ -26,7 +26,7 @@ const cleanEmptyKeys = (stateObj: Record) => { export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; -export interface DashboardAppLocatorParams extends SerializableState { +export interface DashboardAppLocatorParams extends SerializableRecord { /** * If given, the dashboard saved object with this id will be loaded. If not given, * a new, unsaved dashboard will be loaded up. @@ -40,7 +40,7 @@ export interface DashboardAppLocatorParams extends SerializableState { /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the @@ -80,7 +80,7 @@ export interface DashboardAppLocatorParams extends SerializableState { /** * List of dashboard panels */ - panels?: SavedDashboardPanel[] & SerializableState; + panels?: SavedDashboardPanel[] & SerializableRecord; /** * Saved query ID diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 89c9adb57214..6dc068cf55f4 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { AppMountParameters, CoreStart, SavedObjectsClientContract, @@ -14,8 +14,8 @@ import { ChromeStart, IUiSettingsClient, PluginInitializerContext, + KibanaExecutionContext, } from 'kibana/public'; - import { History } from 'history'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; @@ -86,6 +86,7 @@ export interface DashboardContainerInput extends ContainerInput { panels: { [panelId: string]: DashboardPanelState; }; + executionContext?: KibanaExecutionContext; } /** @@ -131,6 +132,7 @@ export type DashboardBuildContext = Pick< dispatchDashboardStateChange: Dispatch; $triggerDashboardRefresh: Subject<{ force?: boolean }>; $onDashboardStateChange: BehaviorSubject; + executionContext?: KibanaExecutionContext; }; export interface DashboardOptions { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 59dfa92cdbce..6e1a6ccf1c86 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -16,7 +16,7 @@ import { createInject, } from '../../common/embeddable/dashboard_container_persistable_state'; import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; -import { SerializableState } from '../../../kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; const embeddableSetupMock = createEmbeddableSetupMock(); const extract = createExtract(embeddableSetupMock); @@ -589,7 +589,7 @@ describe('dashboard', () => { it('runs migrations on by value panels only', () => { const newEmbeddableSetupMock = createEmbeddableSetupMock(); newEmbeddableSetupMock.getAllMigrations.mockImplementation(() => ({ - '7.13.0': (state: SerializableState) => { + '7.13.0': (state: SerializableRecord) => { state.superCoolKey = 'ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH'; return state; }, diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ceb77ba1b2f9..7848a2e46487 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Serializable } from '@kbn/utility-types'; import { get, flow, mapValues } from 'lodash'; import { SavedObjectAttributes, @@ -29,7 +30,6 @@ import { mergeMigrationFunctionMaps, MigrateFunction, MigrateFunctionsObject, - SerializableValue, } from '../../../kibana_utils/common'; import { replaceIndexPatternReference } from './replace_index_pattern_reference'; @@ -154,8 +154,8 @@ function createExtractPanelReferencesMigration( } type ValueOrReferenceInput = SavedObjectEmbeddableInput & { - attributes?: SerializableValue; - savedVis?: SerializableValue; + attributes?: Serializable; + savedVis?: Serializable; }; // Runs the embeddable migrations on each panel diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 12820fc08310..4febb8b5555c 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index 08cda6eb59fb..367234e9ff4f 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { Filter } from '@kbn/es-query'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; +import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from '../../../../core/types'; export const extract = (filters: Filter[]) => { @@ -51,7 +51,7 @@ export const inject = (filters: Filter[], references: SavedObjectReference[]) => }); }; -export const telemetry = (filters: SerializableState, collector: unknown) => { +export const telemetry = (filters: SerializableRecord, collector: unknown) => { return {}; }; diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index f55bfe24cd46..1a70a41e72dd 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import type { SerializableRecord } from '@kbn/utility-types'; import { Assign, Ensure } from '@kbn/utility-types'; import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; @@ -23,23 +24,16 @@ import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; import { parseTimeShift } from './utils'; -type State = string | number | boolean | null | undefined | SerializableState; - -/** @internal **/ -export interface SerializableState { - [key: string]: State | State[]; -} - /** @public **/ export type AggConfigSerialized = Ensure< { type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState; + params?: {} | SerializableRecord; schema?: string; }, - SerializableState + SerializableRecord >; export type AggConfigOptions = Assign; @@ -311,7 +305,7 @@ export class AggConfig { id: this.id, enabled: this.enabled, type: this.type && this.type.name, - params: outParams as SerializableState, + params: outParams as SerializableRecord, ...(this.schema && { schema: this.schema }), }; } @@ -333,7 +327,7 @@ export class AggConfig { */ toSerializedFieldFormat(): | {} - | Ensure, SerializableState> { + | Ensure, SerializableRecord> { return this.type ? this.type.getSerializedFormat(this) : {}; } diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index 0a1ac2289b07..ef5cd85e6f5a 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -12,7 +12,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; -import { SerializableState } from 'src/plugins/expressions/common'; +import { SerializableRecord } from '@kbn/utility-types'; describe('Histogram Agg', () => { let aggTypesDependencies: AggTypesDependencies; @@ -256,7 +256,7 @@ describe('Histogram Agg', () => { }); (aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 }); const serializedAgg = aggConfigs.aggs[0].serialize(); - const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval; + const serializedIntervalParam = (serializedAgg.params as SerializableRecord).used_interval; expect(serializedIntervalParam).toBe(500); const freshHistogramAggConfig = getAggConfigs({ interval: 100, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5f63dacd765d..f47c3d55fa68 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -106,6 +106,7 @@ import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; import { SchemaTypeError } from '@kbn/config-schema'; +import { SerializableRecord } from '@kbn/utility-types'; import { SerializedFieldFormat as SerializedFieldFormat_3 } from 'src/plugins/expressions/common'; import { StartServicesAccessor } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; @@ -200,8 +201,7 @@ export class AggConfig { toExpressionAst(): ExpressionAstExpression | undefined; // @deprecated (undocumented) toJSON(): AggConfigSerialized; - // Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts - toSerializedFieldFormat(): {} | Ensure, SerializableState_2>; + toSerializedFieldFormat(): {} | Ensure, SerializableRecord>; // (undocumented) get type(): IAggType; set type(type: IAggType); @@ -227,7 +227,7 @@ export class AggConfigs { type: string; enabled?: boolean | undefined; id?: string | undefined; - params?: {} | import("./agg_config").SerializableState | undefined; + params?: {} | import("@kbn/utility-types").SerializableRecord | undefined; schema?: string | undefined; }, "schema" | "enabled" | "id" | "params"> & Pick<{ type: string | IAggType; @@ -330,9 +330,9 @@ export type AggConfigSerialized = Ensure<{ type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState_2; + params?: {} | SerializableRecord; schema?: string; -}, SerializableState_2>; +}, SerializableRecord>; // Warning: (ae-missing-release-tag) "AggFunctionsMapping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -792,7 +792,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -943,7 +943,7 @@ export class FilterManager implements PersistableStateService { static setFiltersStore(filters: Filter_2[], store: FilterStateStore, shouldOverrideStore?: boolean): void; setGlobalFilters(newGlobalFilters: Filter_2[]): void; // (undocumented) - telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; + telemetry: (filters: import("@kbn/utility-types").SerializableRecord, collector: unknown) => {}; } // Warning: (ae-missing-release-tag) "generateFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index bcae190b5077..34af80a483e6 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -228,15 +228,15 @@ export class FilterManager implements PersistableStateService { }); } - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public extract = extract as any; - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public inject = inject as any; public telemetry = telemetry; - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public migrateToLatest = migrateToLatest as any; public getAllMigrations = getAllMigrations; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index c4a6ae3a516d..cf3de20fea50 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -64,7 +64,7 @@ export function getFunctionDefinition({ timeFields: args.timeFields, timeRange: get(input, 'timeRange', undefined), getNow, - executionContext: getExecutionContext()?.toJSON(), + executionContext: getExecutionContext(), }) ) ); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx index 57e586eaf12f..03d9a8d2b35b 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx @@ -122,7 +122,7 @@ class DataDownloadOptions extends Component {button} diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index e655a19e46f8..43853aa0ea93 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -33,23 +33,24 @@ export function registerBsearchRoute( onBatchItem: async ({ request: requestData, options }) => { const search = getScoped(request); const { executionContext, ...restOptions } = options || {}; - if (executionContext) executionContextService.set(executionContext); - return search - .search(requestData, restOptions) - .pipe( - first(), - catchError((err) => { - // Re-throw as object, to get attributes passed to the client - // eslint-disable-next-line no-throw-literal - throw { - message: err.message, - statusCode: err.statusCode, - attributes: err.errBody?.error, - }; - }) - ) - .toPromise(); + return executionContextService.withContext(executionContext, () => + search + .search(requestData, restOptions) + .pipe( + first(), + catchError((err) => { + // Re-throw as object, to get attributes passed to the client + // eslint-disable-next-line no-throw-literal + throw { + message: err.message, + statusCode: err.statusCode, + attributes: err.errBody?.error, + }; + }) + ) + .toPromise() + ); }, }; }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 761d6f9345b0..121cd8ebc0af 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -60,6 +60,7 @@ import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kiba import { SavedObjectsFindOptions } from 'kibana/server'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { SerializableRecord } from '@kbn/utility-types'; import { SerializedFieldFormat as SerializedFieldFormat_3 } from 'src/plugins/expressions/common'; import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; @@ -124,7 +125,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 6e4e89f7538f..3687604e05e2 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index c17b2341fd42..bbb33ac5b2f9 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index e13795567445..96888a07be68 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -10,13 +10,16 @@ import { DiscoverServices } from '../build_services'; import { dataPluginMock } from '../../../data/public/mocks'; import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { + CONTEXT_STEP_SETTING, DEFAULT_COLUMNS_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../common'; import { savedSearchMock } from './saved_search'; import { UI_SETTINGS } from '../../../data/common'; import { TopNavMenu } from '../../../navigation/public'; +import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; const dataPlugin = dataPluginMock.createStartContract(); export const discoverServiceMock = ({ @@ -49,10 +52,16 @@ export const discoverServiceMock = ({ return []; } else if (key === UI_SETTINGS.META_FIELDS) { return []; - } else if (key === SAMPLE_SIZE_SETTING) { - return 250; + } else if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } else if (key === CONTEXT_STEP_SETTING) { + return 5; } else if (key === SORT_DEFAULT_ORDER_SETTING) { return 'desc'; + } else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) { + return false; + } else if (key === SAMPLE_SIZE_SETTING) { + return 250; } }, isDefault: (key: string) => { diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 727f941f5fee..e9da3a7c4784 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -22,10 +22,7 @@ export enum SurrDocType { } export type EsHitRecord = Required< - Pick< - estypes.SearchResponse['hits']['hits'][number], - '_id' | 'fields' | 'sort' | '_index' | '_version' - > + Pick > & { _source?: Record; _score?: number; diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 810be94ce24b..b79936bd6f38 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -7,25 +7,40 @@ */ import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { getServices } from '../../kibana_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { TotalDocuments } from '../apps/main/components/total_documents/total_documents'; + +export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { + totalHitCount: number; +} export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( )); -export function DiscoverGridEmbeddable(props: DiscoverGridProps) { +export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); return ( - + + {props.totalHitCount !== 0 && ( + + + + )} + + + + ); } diff --git a/src/plugins/discover/public/application/angular/directives/render_complete.ts b/src/plugins/discover/public/application/angular/directives/render_complete.ts deleted file mode 100644 index b62820bf876d..000000000000 --- a/src/plugins/discover/public/application/angular/directives/render_complete.ts +++ /dev/null @@ -1,20 +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 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 { IScope } from 'angular'; -import { RenderCompleteListener } from '../../../../../kibana_utils/public'; - -export function createRenderCompleteDirective() { - return { - controller($scope: IScope, $element: JQLite) { - const el = $element[0]; - const renderCompleteListener = new RenderCompleteListener(el); - $scope.$on('$destroy', renderCompleteListener.destroy); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap deleted file mode 100644 index fd0c1b4b2af8..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerButtons 1`] = ` - - - - - - - - -`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap deleted file mode 100644 index 96d2994bbe68..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerText without crashing 1`] = ` -
- 1–2 of 3 -
-`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts deleted file mode 100644 index 180da83beb2a..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts +++ /dev/null @@ -1,20 +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 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 { ToolBarPagerText } from './tool_bar_pager_text'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerTextDirective(reactDirective: any) { - return reactDirective(ToolBarPagerText); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerButtonsDirective(reactDirective: any) { - return reactDirective(ToolBarPagerButtons); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx deleted file mode 100644 index 061dae2d5065..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { findTestSubject } from '@elastic/eui/lib/test'; - -test('it renders ToolBarPagerButtons', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ToolBarPagerButtons with clickable next and previous button', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(1); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(1); -}); - -test('it renders ToolBarPagerButtons with disabled next and previous button', () => { - const props = { - hasPreviousPage: false, - hasNextPage: false, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(0); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(0); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx deleted file mode 100644 index d82522016316..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx +++ /dev/null @@ -1,59 +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 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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -interface Props { - hasPreviousPage: boolean; - hasNextPage: boolean; - onPageNext: () => void; - onPagePrevious: () => void; -} - -export function ToolBarPagerButtons(props: Props) { - return ( - - - props.onPagePrevious()} - isDisabled={!props.hasPreviousPage} - data-test-subj="btnPrevPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel', - { - defaultMessage: 'Previous page in table', - } - )} - /> - - - props.onPageNext()} - isDisabled={!props.hasNextPage} - data-test-subj="btnNextPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', - { - defaultMessage: 'Next page in table', - } - )} - /> - - - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss deleted file mode 100644 index 446e852f51e0..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss +++ /dev/null @@ -1,5 +0,0 @@ -.kbnDocTable__toolBarText { - line-height: $euiLineHeight; - color: #69707D; - white-space: nowrap; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx deleted file mode 100644 index a8ebfcc9bb31..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 React from 'react'; -import { renderWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerText } from './tool_bar_pager_text'; - -test('it renders ToolBarPagerText without crashing', () => { - const props = { - startItem: 1, - endItem: 2, - totalItems: 3, - }; - const wrapper = renderWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx deleted file mode 100644 index 5db68952b69c..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ /dev/null @@ -1,31 +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 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 React from 'react'; -import './tool_bar_pager_text.scss'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; - -interface Props { - startItem: number; - endItem: number; - totalItems: number; -} - -export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { - return ( - -
- -
-
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js deleted file mode 100644 index 1a3b34c45d05..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ /dev/null @@ -1,474 +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 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 angular from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { getFakeRow } from '../../../../__fixtures__/fake_row'; -import $ from 'jquery'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../kibana_services'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../../kibana_legacy/public/angular_bootstrap'; -import { getInnerAngularModule } from '../../get_inner_angular'; -import { createBrowserHistory } from 'history'; - -const fakeRowVals = { - time: 'time_formatted', - bytes: 'bytes_formatted', - '@timestamp': '@timestamp_formatted', - request_body: 'request_body_formatted', -}; - -describe('Doc Table', () => { - const core = coreMock.createStart(); - const dataMock = dataPluginMock.createStartContract(); - let $parentScope; - let $scope; - let $elementScope; - let timeout; - let registry = []; - - // Stub out a minimal mapping of 4 fields - let mapping; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(function (index) { - return $(this).slice(index); - }); - angular.element.prototype.filter = jest.fn(function (condition) { - return $(this).filter(condition); - }); - angular.element.prototype.toggle = jest.fn(function (name) { - return $(this).toggle(name); - }); - angular.element.prototype.is = jest.fn(function (name) { - return $(this).is(name); - }); - setServices({ - uiSettings: core.uiSettings, - filterManager: dataMock.query.filterManager, - addBasePath: (path) => path, - }); - - setDocViewsRegistry({ - addDocView(view) { - registry.push(view); - }, - getDocViewsSorted() { - return registry; - }, - resetRegistry: () => { - registry = []; - }, - }); - - getInnerAngularModule( - 'app/discover', - core, - { - data: dataMock, - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach( - angular.mock.inject(function ($rootScope, Private, $timeout) { - $parentScope = $rootScope; - timeout = $timeout; - $parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - mapping = $parentScope.indexPattern.fields; - - // Stub `getConverterFor` for a field in the indexPattern to return mock data. - - const convertFn = (value, type, options) => { - const fieldName = _.get(options, 'field.name', null); - return fakeRowVals[fieldName] || ''; - }; - $parentScope.indexPattern.getFormatterForField = () => ({ - convert: convertFn, - getConverterFor: () => convertFn, - }); - }) - ); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - delete angular.element.prototype.toggle; - delete angular.element.prototype.is; - }); - - // Sets up the directive, take an element, and a list of properties to attach to the parent scope. - const init = function ($elem, props) { - angular.mock.inject(function ($compile) { - _.assign($parentScope, props); - const el = $compile($elem)($parentScope); - $elementScope = el.scope(); - el.scope().$digest(); - $scope = el.isolateScope(); - }); - }; - - const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); - }; - - // For testing column removing/adding for the header and the rows - const columnTests = function (elemType, parentElem) { - test('should create a time column if the timefield is defined', () => { - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(1); - }); - - test('should be able to add and remove columns', () => { - let childElems; - - // Should include a column for toggling and the time column by default - $parentScope.columns = ['bytes']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('bytes'); - - $parentScope.columns = ['bytes', 'request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(3); - expect($(childElems[2]).text()).toContain('request_body'); - - $parentScope.columns = ['request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('request_body'); - }); - - test('should create only the toggle column if there is no timeField', () => { - delete $scope.indexPattern.timeFieldName; - $scope.$digest(); - timeout.flush(); - - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(0); - }); - }; - - describe('kbnTableRow', () => { - const $elem = $( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row, - columns: [], - sorting: [], - filter: sinon.spy(), - maxLength: 50, - }); - }); - afterEach(() => { - destroy(); - }); - - describe('adding and removing columns', () => { - columnTests('[data-test-subj~="docTableField"]', $elem); - }); - - describe('details row', () => { - test('should be an empty tr by default', () => { - expect($elem.next().is('tr')).toBe(true); - expect($elem.next().text()).toBe(''); - }); - - test('should expand the detail row when the toggle arrow is clicked', () => { - $elem.children(':first-child').click(); - expect($elem.next().text()).not.toBe(''); - }); - - describe('expanded', () => { - let $details; - beforeEach(() => { - // Open the row - $scope.toggleRow(); - timeout.flush(); - $details = $elem.next(); - }); - afterEach(() => { - // Close the row - $scope.toggleRow(); - }); - - test('should be a tr with something in it', () => { - expect($details.is('tr')).toBe(true); - expect($details.text()).toBeTruthy(); - }); - }); - }); - }); - - describe('kbnTableRow meta', () => { - const $elem = angular.element( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row: row, - columns: [], - sorting: [], - filtering: sinon.spy(), - maxLength: 50, - }); - - // Open the row - $scope.toggleRow(); - $scope.$digest(); - timeout.flush(); - $elem.next(); - }); - - afterEach(() => { - destroy(); - }); - - /** this no longer works with the new plugin approach - test('should render even when the row source contains a field with the same name as a meta field', () => { - setTimeout(() => { - //this should be overridden by later changes - }, 100); - expect($details.find('tr').length).toBe(_.keys($parentScope.indexPattern.flattenHit($scope.row)).length); - }); */ - }); - - describe('row diffing', () => { - let $row; - let $scope; - let $root; - let $before; - - beforeEach( - angular.mock.inject(function ($rootScope, $compile, Private) { - $root = $rootScope; - $root.row = getFakeRow(0, mapping); - $root.columns = ['_source']; - $root.sorting = []; - $root.filtering = sinon.spy(); - $root.maxLength = 50; - $root.mapping = mapping; - $root.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - $row = $('').attr({ - 'kbn-table-row': 'row', - columns: 'columns', - sorting: 'sorting', - filtering: 'filtering', - 'index-pattern': 'indexPattern', - }); - - $scope = $root.$new(); - $compile($row)($scope); - $root.$apply(); - - $before = $row.find('td'); - expect($before).toHaveLength(3); - expect($before.eq(0).text().trim()).toBe(''); - expect($before.eq(1).text().trim()).toMatch(/^time_formatted/); - }) - ); - - afterEach(() => { - $row.remove(); - }); - - test('handles a new column', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - }); - - test('handles two new columns at once', () => { - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(5); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles three new columns in odd places', () => { - $root.columns = ['@timestamp', 'bytes', '_source', 'request_body']; - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after[4].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(5).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles a removed column', () => { - _.pull($root.columns, '_source'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles two removed columns', () => { - // first add a column - $root.columns.push('@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.pop(); - $root.columns.pop(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles three removed random columns', () => { - // first add two column - $root.columns.push('@timestamp', 'bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(5); - - $root.columns[0] = false; // _source - $root.columns[2] = false; // bytes - $root.columns = $root.columns.filter(Boolean); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(3); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - }); - - test('handles two columns with the same content', () => { - const tempVal = fakeRowVals.request_body; - fakeRowVals.request_body = 'bytes_formatted'; - - $root.columns.length = 0; - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after.eq(2).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - fakeRowVals.request_body = tempVal; - }); - - test('handles two columns swapping position', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[3].outerHTML); - expect($after[3].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles four columns all reversing position', () => { - $root.columns.push('bytes', 'response', '@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(6); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[5].outerHTML); - expect($after[3].outerHTML).toBe($mid[4].outerHTML); - expect($after[4].outerHTML).toBe($mid[3].outerHTML); - expect($after[5].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles multiple columns with the same name', () => { - $root.columns.push('bytes', 'bytes', 'bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(5).text().trim()).toMatch(/^bytes_formatted/); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts deleted file mode 100644 index 0f6c86df0db6..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ /dev/null @@ -1,37 +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 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 { TableHeader } from './table_header/table_header'; -import { getServices } from '../../../../kibana_services'; -import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createTableHeaderDirective(reactDirective: any) { - const { uiSettings: config } = getServices(); - - return reactDirective( - TableHeader, - [ - ['columns', { watchDepth: 'collection' }], - ['hideTimeColumn', { watchDepth: 'value' }], - ['indexPattern', { watchDepth: 'reference' }], - ['isShortDots', { watchDepth: 'value' }], - ['onChangeSortOrder', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['sortOrder', { watchDepth: 'collection' }], - ], - { restrict: 'A' }, - { - hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - isShortDots: config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), - defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), - } - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts deleted file mode 100644 index 1d6956fc8092..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { find } from 'lodash'; -import $ from 'jquery'; -import openRowHtml from './table_row/open.html'; -import detailsHtml from './table_row/details.html'; -import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { getServices } from '../../../../kibana_services'; -import { getContextUrl } from '../../../helpers/get_context_url'; -import { formatRow, formatTopLevelObject } from '../../helpers'; -import { truncateByHeight } from './table_row/truncate_by_height'; -import { cell } from './table_row/cell'; - -// guesstimate at the minimum number of chars wide cells in the table should be -const MIN_LINE_LENGTH = 20; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createTableRowDirective($compile: ng.ICompileService) { - return { - restrict: 'A', - scope: { - columns: '=', - filter: '=', - indexPattern: '=', - row: '=kbnTableRow', - onAddColumn: '=?', - onRemoveColumn: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $el.after(''); - $el.empty(); - - // when we compile the details, we use this $scope - let $detailsScope: LazyScope; - - // when we compile the toggle button in the summary, we use this $scope - let $toggleScope; - - // toggle display of the rows details, a full list of the fields from each row - $scope.toggleRow = () => { - const $detailsTr = $el.next(); - - $scope.open = !$scope.open; - - /// - // add/remove $details children - /// - - $detailsTr.toggle($scope.open); - - if (!$scope.open) { - // close the child scope if it exists - $detailsScope.$destroy(); - // no need to go any further - return; - } else { - $detailsScope = $scope.$new(); - } - - // empty the details and rebuild it - $detailsTr.html(detailsHtml); - $detailsScope.row = $scope.row; - $detailsScope.hit = $scope.row; - $detailsScope.uriEncodedId = encodeURIComponent($detailsScope.hit._id); - - $compile($detailsTr)($detailsScope); - }; - - $scope.$watchMulti(['indexPattern.timeFieldName', 'row.highlight', '[]columns'], () => { - createSummaryRow($scope.row); - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - $scope.inlineFilter = function inlineFilter($event: any, type: string) { - const column = $($event.currentTarget).data().column; - const field = $scope.indexPattern.fields.getByName(column); - $scope.filter(field, $scope.flattenedRow[column], type); - }; - - $scope.getContextAppHref = () => { - return getContextUrl( - $scope.row._id, - $scope.indexPattern.id, - $scope.columns, - getServices().filterManager, - getServices().addBasePath - ); - }; - - $scope.getSingleDocHref = () => { - return getServices().addBasePath( - `/app/discover#/doc/${$scope.indexPattern.id}/${ - $scope.row._index - }?id=${encodeURIComponent($scope.row._id)}` - ); - }; - - // create a tr element that lists the value for each *column* - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function createSummaryRow(row: any) { - const indexPattern = $scope.indexPattern; - $scope.flattenedRow = indexPattern.flattenHit(row); - - // We just create a string here because its faster. - const newHtmls = [openRowHtml]; - - const mapping = indexPattern.fields.getByName; - const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - if (indexPattern.timeFieldName && !hideTimeColumn) { - newHtmls.push( - cell({ - timefield: true, - formatted: _displayField(row, indexPattern.timeFieldName), - filterable: mapping(indexPattern.timeFieldName).filterable && $scope.filter, - column: indexPattern.timeFieldName, - }) - ); - } - - if ($scope.columns.length === 0 && $scope.useNewFieldsApi) { - const formatted = formatRow(row, indexPattern); - - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted, - filterable: false, - column: '__document__', - }) - ); - } else { - $scope.columns.forEach(function (column: string) { - const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter; - if ($scope.useNewFieldsApi && !mapping(column) && !row.fields[column]) { - const innerColumns = Object.fromEntries( - Object.entries(row.fields).filter(([key]) => { - return key.indexOf(`${column}.`) === 0; - }) - ); - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted: formatTopLevelObject(row, innerColumns, indexPattern), - filterable: false, - column, - }) - ); - } else { - newHtmls.push( - cell({ - timefield: false, - sourcefield: column === '_source', - formatted: _displayField(row, column, true), - filterable: isFilterable, - column, - }) - ); - } - }); - } - - let $cells = $el.children(); - newHtmls.forEach(function (html, i) { - const $cell = $cells.eq(i); - if ($cell.data('discover:html') === html) return; - - const reuse = find($cells.slice(i + 1), (c) => { - return $.data(c, 'discover:html') === html; - }); - - const $target = reuse ? $(reuse).detach() : $(html); - $target.data('discover:html', html); - const $before = $cells.eq(i - 1); - if ($before.length) { - $before.after($target); - } else { - $el.append($target); - } - - // rebuild cells since we modified the children - $cells = $el.children(); - - if (!reuse) { - $toggleScope = $scope.$new(); - $compile($target)($toggleScope); - } - }); - - if ($scope.open) { - $detailsScope.row = row; - } - - // trim off cells that were not used rest of the cells - $cells.filter(':gt(' + (newHtmls.length - 1) + ')').remove(); - dispatchRenderComplete($el[0]); - } - - /** - * Fill an element with the value of a field - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function _displayField(row: any, fieldName: string, truncate = false) { - const indexPattern = $scope.indexPattern; - const text = indexPattern.formatField(row, fieldName); - - if (truncate && text.length > MIN_LINE_LENGTH) { - return truncateByHeight({ - body: text, - }); - } - - return text; - } - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts deleted file mode 100644 index c6d0d324b9bc..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts +++ /dev/null @@ -1,138 +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 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 { cell } from './cell'; - -describe('cell renderer', () => { - it('renders a cell without filter buttons if it is not filterable', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a cell with filter buttons if it is filterable', () => { - expect( - cell({ - filterable: true, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: true, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a field that is neither a timefield or sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders the "formatted" contents without any manipulation', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot(` - "
 hey you can put HTML & stuff in here 
- " - `); - }); - - it('escapes the contents of "column" within the "data-column" attribute', () => { - expect( - cell({ - filterable: true, - column: '', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts deleted file mode 100644 index 8138e0f4a4fd..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { escape } from 'lodash'; -import cellWithFilters from './cell_with_buttons.html'; -import cellWithoutFilters from './cell_without_buttons.html'; - -const TAGS_WITH_WS = />\s+<'); -} - -const cellWithFiltersTemplate = noWhiteSpace(cellWithFilters); -const cellWithoutFiltersTemplate = noWhiteSpace(cellWithoutFilters); - -interface CellProps { - timefield: boolean; - sourcefield?: boolean; - formatted: string; - filterable: boolean; - column: string; -} - -export const cell = (props: CellProps) => { - let classes = ''; - let extraAttrs = ''; - if (props.timefield) { - classes = 'eui-textNoWrap'; - extraAttrs = 'width="1%"'; - } else if (props.sourcefield) { - classes = 'eui-textBreakAll eui-textBreakWord'; - } else { - classes = 'kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord'; - } - - if (props.filterable) { - const escapedColumnContents = escape(props.column); - return cellWithFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('__column__', escapedColumnContents) - .replace('__column__', escapedColumnContents) - .replace('', props.formatted); - } - return cellWithoutFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('', props.formatted); -}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html deleted file mode 100644 index 99c65e603401..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html deleted file mode 100644 index 8dc33cbfb835..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html deleted file mode 100644 index faa3d51c19fe..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ /dev/null @@ -1,53 +0,0 @@ - -
-
-
-
- -
-
-

-
-
-
-
-
-
- -
-
- -
-
-
-
-
- -
- - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html deleted file mode 100644 index 1a5d974a1b08..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts deleted file mode 100644 index 70d846558923..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts +++ /dev/null @@ -1,22 +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 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 { truncateByHeight } from './truncate_by_height'; - -describe('truncateByHeight', () => { - it('renders input without any formatting or escaping', () => { - expect( - truncateByHeight({ - body: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot( - `"
 hey you can put HTML & stuff in here 
"` - ); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx deleted file mode 100644 index 19913ed6de87..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx +++ /dev/null @@ -1,85 +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 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 React, { useRef, useEffect } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IScope } from 'angular'; -import { getServices } from '../../../kibana_services'; -import { DocTableLegacyProps, injectAngularElement } from './create_doc_table_react'; - -type AngularEmbeddableScope = IScope & { renderProps?: DocTableEmbeddableProps }; - -export interface DocTableEmbeddableProps extends Partial { - refs: HTMLElement; -} - -function getRenderFn(domNode: Element, props: DocTableEmbeddableProps) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - throw e; - } - }; -} - -export function DiscoverDocTableEmbeddable(props: DocTableEmbeddableProps) { - return ( - - - - ); -} - -function DocTableLegacyInner(renderProps: DocTableEmbeddableProps) { - const scope = useRef(); - - useEffect(() => { - if (renderProps.refs && !scope.current) { - const fn = getRenderFn(renderProps.refs, renderProps); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope?.current) { - scope.current.renderProps = { ...renderProps }; - scope.current.$applyAsync(); - } - }, [renderProps]); - - useEffect(() => { - return () => { - scope.current?.$destroy(); - }; - }, []); - return ; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx deleted file mode 100644 index 73a67310bf4b..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ /dev/null @@ -1,173 +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 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 angular, { auto, ICompileService, IScope } from 'angular'; -import { render } from 'react-dom'; -import React, { useRef, useEffect, useState, useCallback } from 'react'; -import type { estypes } from '@elastic/elasticsearch'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getServices, IndexPattern } from '../../../kibana_services'; -import { IndexPatternField } from '../../../../../data/common'; -import { SkipBottomButton } from '../../apps/main/components/skip_bottom_button'; - -export interface DocTableLegacyProps { - columns: string[]; - searchDescription?: string; - searchTitle?: string; - onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - rows: estypes.SearchHit[]; - indexPattern: IndexPattern; - minimumVisibleRows?: number; - onAddColumn?: (column: string) => void; - onBackToTop: () => void; - onSort?: (sort: string[][]) => void; - onMoveColumn?: (columns: string, newIdx: number) => void; - onRemoveColumn?: (column: string) => void; - sampleSize: number; - sort?: string[][]; - useNewFieldsApi?: boolean; -} -export interface AngularDirective { - template: string; -} -export type AngularScope = IScope & { renderProps?: DocTableLegacyProps }; - -/** - * Compiles and injects the give angular template into the given dom node - * returns a function to cleanup the injected angular element - */ -export async function injectAngularElement( - domNode: Element, - template: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderProps: any, - injector: auto.IInjectorService -) { - const rootScope: IScope = injector.get('$rootScope'); - const $compile: ICompileService = injector.get('$compile'); - const newScope = Object.assign(rootScope.$new(), { renderProps }); - - const $target = angular.element(domNode); - const $element = angular.element(template); - - newScope.$apply(() => { - const linkFn = $compile($element); - $target.empty().append($element); - linkFn(newScope); - }); - - return newScope; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getRenderFn(domNode: Element, props: any) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - render(
error
, domNode); - } - }; -} - -export function DocTableLegacy(renderProps: DocTableLegacyProps) { - const ref = useRef(null); - const scope = useRef(); - const [rows, setRows] = useState(renderProps.rows); - const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); - const onSkipBottomButtonClick = useCallback(async () => { - // delay scrolling to after the rows have been rendered - const bottomMarker = document.getElementById('discoverBottomMarker'); - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - // show all the rows - setMinimumVisibleRows(renderProps.rows.length); - - while (renderProps.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { - await wait(50); - } - bottomMarker!.focus(); - await wait(50); - bottomMarker!.blur(); - }, [setMinimumVisibleRows, renderProps.rows]); - - useEffect(() => { - setMinimumVisibleRows(50); - setRows(renderProps.rows); - }, [renderProps.rows, setMinimumVisibleRows]); - - useEffect(() => { - if (ref && ref.current && !scope.current) { - const fn = getRenderFn(ref.current, { ...renderProps, rows, minimumVisibleRows }); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope && scope.current) { - scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows }; - scope.current.$applyAsync(); - } - }, [renderProps, minimumVisibleRows, rows]); - - useEffect(() => { - return () => { - if (scope.current) { - scope.current.$destroy(); - } - }; - }, []); - return ( -
- -
- {renderProps.rows.length === renderProps.sampleSize ? ( -
- - - - -
- ) : ( - - ​ - - )} -
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html deleted file mode 100644 index ecd7aa8f3dcf..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ /dev/null @@ -1,121 +0,0 @@ -
-
-
-
-
- {{ limitedResultsWarning }} -
- - - -
-
-
- - - - - -
-
- - -
- - - - - - -
- -
- -
-
- - -
- -

-

-
-
diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js deleted file mode 100644 index 097f32965b14..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ /dev/null @@ -1,140 +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 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 angular from 'angular'; -import _ from 'lodash'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import { createBrowserHistory } from 'history'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../__fixtures__/stubbed_logstash_index_pattern'; -import hits from '../../../__fixtures__/real_hits'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; -import { setScopedHistory, setServices } from '../../../kibana_services'; -import { getInnerAngularModule } from '../get_inner_angular'; - -let $parentScope; - -let $scope; - -let $timeout; - -let indexPattern; - -const init = function ($elem, props) { - angular.mock.inject(function ($rootScope, $compile, _$timeout_) { - $timeout = _$timeout_; - $parentScope = $rootScope; - _.assign($parentScope, props); - - $compile($elem)($parentScope); - - // I think the prereq requires this? - $timeout(() => { - $elem.scope().$digest(); - }, 0); - - $scope = $elem.isolateScope(); - }); -}; - -const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); -}; - -describe('docTable', () => { - const core = coreMock.createStart(); - let $elem; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(() => { - return null; - }); - angular.element.prototype.filter = jest.fn(() => { - return { - remove: jest.fn(), - }; - }); - setServices({ - uiSettings: core.uiSettings, - }); - getInnerAngularModule( - 'app/discover', - core, - { - data: dataPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach(() => { - $elem = angular.element(` - - `); - angular.mock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }); - init($elem, { - indexPattern, - hits: [...hits], - totalHitCount: hits.length, - columns: [], - sorting: ['@timestamp', 'desc'], - }); - $scope.$digest(); - }); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - destroy(); - }); - - test('should compile', () => { - expect($elem.text()).toBeTruthy(); - }); - - test('should have an addRows function that increases the row count', () => { - expect($scope.addRows).toBeInstanceOf(Function); - $scope.$digest(); - expect($scope.limit).toBe(50); - $scope.addRows(); - expect($scope.limit).toBe(100); - }); - - test('should reset the row limit when results are received', () => { - $scope.limit = 100; - expect($scope.limit).toBe(100); - $scope.hits = [...hits]; - $scope.$digest(); - expect($scope.limit).toBe(50); - }); - - test('should have a header and a table element', () => { - $scope.$digest(); - - expect($elem.find('thead').length).toBe(1); - expect($elem.find('table').length).toBe(1); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts deleted file mode 100644 index 64c045a68229..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ /dev/null @@ -1,98 +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 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 html from './doc_table.html'; -import { dispatchRenderComplete } from '../../../../../kibana_utils/public'; -import { SAMPLE_SIZE_SETTING } from '../../../../common'; -// @ts-expect-error -import { getLimitedSearchResultsMessage } from './doc_table_strings'; -import { getServices } from '../../../kibana_services'; -import './index.scss'; - -export interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDocTableDirective(pagerFactory: any, $filter: any) { - return { - restrict: 'E', - template: html, - scope: { - sorting: '=', - columns: '=', - hits: '=', - totalHitCount: '=', - indexPattern: '=', - isLoading: '=?', - infiniteScroll: '=?', - filter: '=?', - minimumVisibleRows: '=?', - onAddColumn: '=?', - onChangeSortOrder: '=?', - onMoveColumn: '=?', - onRemoveColumn: '=?', - inspectorAdapters: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $scope.persist = { - sorting: $scope.sorting, - columns: $scope.columns, - }; - - const limitTo = $filter('limitTo'); - const calculateItemsOnPage = () => { - $scope.pager.setTotalItems($scope.hits.length); - $scope.pageOfItems = limitTo($scope.hits, $scope.pager.pageSize, $scope.pager.startIndex); - }; - - $scope.limitedResultsWarning = getLimitedSearchResultsMessage( - getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500) - ); - - $scope.addRows = function () { - $scope.limit += 50; - }; - - $scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => { - $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50); - }); - - $scope.$watch('hits', (hits: unknown[]) => { - if (!hits) return; - - // Reset infinite scroll limit - $scope.limit = $scope.minimumVisibleRows || 50; - - if (hits.length === 0) { - dispatchRenderComplete($el[0]); - } - - if ($scope.infiniteScroll) return; - $scope.pager = pagerFactory.create(hits.length, 50, 1); - calculateItemsOnPage(); - }); - - $scope.pageOfItems = []; - $scope.onPageNext = () => { - $scope.pager.nextPage(); - calculateItemsOnPage(); - }; - - $scope.onPagePrevious = () => { - $scope.pager.previousPage(); - calculateItemsOnPage(); - }; - - $scope.shouldShowLimitedResultsWarning = () => - !$scope.pager.hasNextPage && $scope.pager.totalItems < $scope.totalHitCount; - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js b/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js deleted file mode 100644 index aac4b9cfe155..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { i18n } from '@kbn/i18n'; - -/** - * A message letting the user know the results that have been retrieved is limited - * to a certain size. - * @param resultCount {Number} - */ -export function getLimitedSearchResultsMessage(resultCount) { - return i18n.translate('discover.docTable.limitedSearchResultLabel', { - defaultMessage: 'Limited to {resultCount} results. Refine your search.', - values: { resultCount }, - }); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts deleted file mode 100644 index 2029354376f2..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ /dev/null @@ -1,69 +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 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 $ from 'jquery'; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createInfiniteScrollDirective() { - return { - restrict: 'E', - scope: { - more: '=', - }, - link: ($scope: LazyScope, $element: JQuery) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let checkTimer: any; - /** - * depending on which version of Discover is displayed, different elements are scrolling - * and have therefore to be considered for calculation of infinite scrolling - */ - const scrollDiv = $element.parents('.dscTable'); - const scrollDivMobile = $(window); - - function onScroll() { - if (!$scope.more) return; - const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; - const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; - const scrollTop = usedScrollDiv.scrollTop(); - const scrollOffset = usedScrollDiv.prop('offsetTop') || 0; - - const winHeight = Number(usedScrollDiv.height()); - const winBottom = Number(winHeight) + Number(scrollTop); - const elTop = $element.get(0).offsetTop || 0; - const remaining = elTop - scrollOffset - winBottom; - - if (remaining <= winHeight) { - $scope[$scope.$$phase ? '$eval' : '$apply'](function () { - $scope.more(); - }); - } - } - - function scheduleCheck() { - if (checkTimer) return; - checkTimer = setTimeout(function () { - checkTimer = null; - onScroll(); - }, 50); - } - - scrollDiv.on('scroll', scheduleCheck); - window.addEventListener('scroll', scheduleCheck); - $scope.$on('$destroy', function () { - clearTimeout(checkTimer); - scrollDiv.off('scroll', scheduleCheck); - window.removeEventListener('scroll', scheduleCheck); - }); - scheduleCheck(); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js deleted file mode 100644 index db99dbe76d99..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js +++ /dev/null @@ -1,10 +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 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 './pager_factory'; -export { Pager } from './pager'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js deleted file mode 100644 index 1bd27a8854ca..000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js +++ /dev/null @@ -1,66 +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 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. - */ - -function clamp(val, min, max) { - return Math.min(Math.max(min, val), max); -} - -export class Pager { - constructor(totalItems, pageSize, startingPage) { - this.currentPage = startingPage; - this.totalItems = totalItems; - this.pageSize = pageSize; - this.startIndex = 0; - this.updateMeta(); - } - - get pageCount() { - return Math.ceil(this.totalItems / this.pageSize); - } - - get hasNextPage() { - return this.currentPage < this.totalPages; - } - - get hasPreviousPage() { - return this.currentPage > 1; - } - - nextPage() { - this.currentPage += 1; - this.updateMeta(); - } - - previousPage() { - this.currentPage -= 1; - this.updateMeta(); - } - - setTotalItems(count) { - this.totalItems = count; - this.updateMeta(); - } - - setPageSize(count) { - this.pageSize = count; - this.updateMeta(); - } - - updateMeta() { - this.totalPages = Math.ceil(this.totalItems / this.pageSize); - this.currentPage = clamp(this.currentPage, 1, this.totalPages); - - this.startItem = (this.currentPage - 1) * this.pageSize + 1; - this.startItem = clamp(this.startItem, 0, this.totalItems); - - this.endItem = this.startItem - 1 + this.pageSize; - this.endItem = clamp(this.endItem, 0, this.totalItems); - - this.startIndex = this.startItem - 1; - } -} diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 992d82795302..5f459c369ce4 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -19,19 +19,8 @@ import { CoreStart, PluginInitializerContext } from 'kibana/public'; import { DataPublicPluginStart } from '../../../../data/public'; import { Storage } from '../../../../kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; -import { createDocTableDirective } from './doc_table'; -import { createTableHeaderDirective } from './doc_table/components/table_header'; -import { - createToolBarPagerButtonsDirective, - createToolBarPagerTextDirective, -} from './doc_table/components/pager'; import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; -import { createTableRowDirective } from './doc_table/components/table_row'; -import { createPagerFactory } from './doc_table/lib/pager/pager_factory'; -import { createInfiniteScrollDirective } from './doc_table/infinite_scroll'; -import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; -import { createRenderCompleteDirective } from './directives/render_complete'; import { configureAppAngularModule, PrivateProvider, @@ -83,7 +72,6 @@ export function initializeInnerAngularModule( createLocalPrivateModule(); createLocalPromiseModule(); createLocalStorageModule(); - createPagerFactoryModule(); createDocTableModule(); initialized = true; } @@ -97,12 +85,10 @@ export function initializeInnerAngularModule( 'discoverI18n', 'discoverPrivate', 'discoverDocTable', - 'discoverPagerFactory', 'discoverPromise', ]) .config(watchMultiDecorator) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)) - .directive('renderComplete', createRenderCompleteDirective); + .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } return angular @@ -116,11 +102,9 @@ export function initializeInnerAngularModule( 'discoverPromise', 'discoverLocalStorageProvider', 'discoverDocTable', - 'discoverPagerFactory', ]) .config(watchMultiDecorator) .run(registerListenEventListener) - .directive('renderComplete', createRenderCompleteDirective) .directive('discover', createDiscoverDirective); } @@ -153,20 +137,9 @@ const createLocalStorageService = function (type: string) { }; }; -function createPagerFactoryModule() { - angular.module('discoverPagerFactory', []).factory('pagerFactory', createPagerFactory); -} - function createDocTableModule() { angular - .module('discoverDocTable', ['discoverPagerFactory', 'react']) - .directive('docTable', createDocTableDirective) - .directive('kbnTableHeader', createTableHeaderDirective) - .directive('toolBarPagerText', createToolBarPagerTextDirective) - .directive('kbnTableRow', createTableRowDirective) - .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) - .directive('kbnInfiniteScroll', createInfiniteScrollDirective) + .module('discoverDocTable', ['react']) .directive('discoverGrid', createDiscoverGridDirective) - .directive('docViewer', createDocViewerDirective) .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 6a7f75b7e81a..a7d9d4581d98 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index e75add7910b7..c4f6415c771f 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -14,5 +14,4 @@ import 'angular-route'; import './discover'; import './doc'; import './context'; -import './doc_viewer'; import './redirect'; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx index 7a13f18997b8..dc3c9ebbc75c 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx @@ -96,7 +96,6 @@ function getProps(timefield?: string) { }) as DataCharts$; return { - isLegacy: false, resetQuery: jest.fn(), savedSearch: savedSearchMock, savedSearchDataChart$: charts$, diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx index 165f708bf708..7d761aa93b80 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -21,7 +21,6 @@ import { DiscoverServices } from '../../../../../build_services'; const TimechartHeaderMemoized = memo(TimechartHeader); const DiscoverHistogramMemoized = memo(DiscoverHistogram); export function DiscoverChart({ - isLegacy, resetQuery, savedSearch, savedSearchDataChart$, @@ -31,7 +30,6 @@ export function DiscoverChart({ stateContainer, timefield, }: { - isLegacy: boolean; resetQuery: () => void; savedSearch: SavedSearch; savedSearchDataChart$: DataCharts$; @@ -135,10 +133,7 @@ export function DiscoverChart({ })} className="dscTimechart" > -
+
) => void) { return getStateColumnActions({ diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts similarity index 92% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 9ef5d45947af..130b43539d9b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -6,17 +6,17 @@ * Side Public License, v 1. */ import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { popularizeField } from '../../../helpers/popularize_field'; -import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../../../common'; +import { popularizeField } from '../../../../../../application/helpers/popularize_field'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../../../apps/main/services/discover_state'; +} from '../../../../../../application/apps/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../../context_state'; -import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +} from '../../../../../../application/angular/context_state'; +import { IndexPattern, IndexPatternsContract } from '../../../../../../../../data/public'; /** * Helper function to provide a fallback to a single _source column if the given array of columns diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx new file mode 100644 index 000000000000..878a9b816262 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx @@ -0,0 +1,108 @@ +/* + * 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 React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n/'; + +interface ToolBarPaginationProps { + pageSize: number; + pageCount: number; + activePage: number; + onPageClick: (page: number) => void; + onPageSizeChange: (size: number) => void; +} + +export const ToolBarPagination = ({ + pageSize, + pageCount, + activePage, + onPageSizeChange, + onPageClick, +}: ToolBarPaginationProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const rowsWord = i18n.translate('discover.docTable.rows', { + defaultMessage: 'rows', + }); + + const onChooseRowsClick = () => setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + + const closePopover = () => setIsPopoverOpen(false); + + const getIconType = (size: number) => { + return size === pageSize ? 'check' : 'empty'; + }; + + const rowsPerPageOptions = [25, 50, 100].map((cur) => ( + { + closePopover(); + onPageSizeChange(cur); + }} + > + {cur} {rowsWord} + + )); + + return ( + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index a75aea716973..5afa26d35b4f 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx similarity index 99% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx index 48ea7ffc4638..7b72e94169cf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { TableHeader } from './table_header'; import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; -import { IndexPattern, IndexPatternField } from '../../../../../kibana_services'; +import { IndexPattern, IndexPatternField } from '../../../../../../../kibana_services'; function getMockIndexPattern() { return ({ diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index 57f7382bd98c..cb8198f1d6d6 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx new file mode 100644 index 000000000000..59ced9d5668a --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 React from 'react'; +import { mountWithIntl, findTestSubject } from '@kbn/test/jest'; +import { TableRow, TableRowProps } from './table_row'; +import { setDocViewsRegistry, setServices } from '../../../../../../kibana_services'; +import { createFilterManagerMock } from '../../../../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { DiscoverServices } from '../../../../../../build_services'; +import { indexPatternWithTimefieldMock } from '../../../../../../__mocks__/index_pattern_with_timefield'; +import { uiSettingsMock } from '../../../../../../__mocks__/ui_settings'; +import { DocViewsRegistry } from '../../../../../doc_views/doc_views_registry'; + +jest.mock('../lib/row_formatter', () => { + const originalModule = jest.requireActual('../lib/row_formatter'); + return { + ...originalModule, + formatRow: () => mocked_document_cell, + }; +}); + +const mountComponent = (props: TableRowProps) => { + return mountWithIntl( + + + + +
+ ); +}; + +const mockHit = { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, +}; + +const mockFilterManager = createFilterManagerMock(); + +describe('Doc table row component', () => { + let mockInlineFilter; + let defaultProps: TableRowProps; + + beforeEach(() => { + mockInlineFilter = jest.fn(); + + defaultProps = ({ + columns: ['_source'], + filter: mockInlineFilter, + indexPattern: indexPatternWithTimefieldMock, + row: mockHit, + useNewFieldsApi: true, + filterManager: mockFilterManager, + addBasePath: (path: string) => path, + hideTimeColumn: true, + } as unknown) as TableRowProps; + + setServices(({ + uiSettings: uiSettingsMock, + } as unknown) as DiscoverServices); + + setDocViewsRegistry(new DocViewsRegistry()); + }); + + it('should render __document__ column', () => { + const component = mountComponent({ ...defaultProps, columns: [] }); + const docTableField = findTestSubject(component, 'docTableField'); + expect(docTableField.first().text()).toBe('mocked_document_cell'); + }); + + it('should render message, _index and bytes fields', () => { + const component = mountComponent({ ...defaultProps, columns: ['message', '_index', 'bytes'] }); + + const fields = findTestSubject(component, 'docTableField'); + expect(fields.first().text()).toBe('mock_message'); + expect(fields.last().text()).toBe('20'); + expect(fields.length).toBe(3); + }); + + describe('details row', () => { + it('should be empty by default', () => { + const component = mountComponent(defaultProps); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + }); + + it('should expand the detail row when the toggle arrow is clicked', () => { + const component = mountComponent(defaultProps); + const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn'); + + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + toggleButton.simulate('click'); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeTruthy(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx new file mode 100644 index 000000000000..886aeffc0666 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -0,0 +1,204 @@ +/* + * 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 React, { Fragment, useCallback, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; +import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; +import { TableCell } from './table_row/table_cell'; +import { ElasticSearchHit, DocViewFilterFn } from '../../../../../doc_views/doc_views_types'; +import { trimAngularSpan } from '../../../../../components/table/table_helper'; +import { getContextUrl } from '../../../../../helpers/get_context_url'; +import { getSingleDocUrl } from '../../../../../helpers/get_single_doc_url'; +import { TableRowDetails } from './table_row_details'; +import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; + +export type DocTableRow = ElasticSearchHit & { + isAnchor?: boolean; +}; + +export interface TableRowProps { + columns: string[]; + filter: DocViewFilterFn; + indexPattern: IndexPattern; + row: DocTableRow; + onAddColumn?: (column: string) => void; + onRemoveColumn?: (column: string) => void; + useNewFieldsApi: boolean; + hideTimeColumn: boolean; + filterManager: FilterManager; + addBasePath: (path: string) => string; +} + +export const TableRow = ({ + columns, + filter, + row, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, +}: TableRowProps) => { + const [open, setOpen] = useState(false); + const docTableRowClassName = classNames('kbnDocTable__row', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'kbnDocTable__row--highlight': row.isAnchor, + }); + const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : ''; + + const flattenedRow = useMemo(() => indexPattern.flattenHit(row), [indexPattern, row]); + const mapping = useMemo(() => indexPattern.fields.getByName, [indexPattern]); + + // toggle display of the rows details, a full list of the fields from each row + const toggleRow = () => setOpen((prevOpen) => !prevOpen); + + /** + * Fill an element with the value of a field + */ + const displayField = (fieldName: string) => { + const text = indexPattern.formatField(row, fieldName); + const formattedField = trimAngularSpan(String(text)); + + // field formatters take care of escaping + // eslint-disable-next-line react/no-danger + const fieldElement = ; + + return
{fieldElement}
; + }; + const inlineFilter = useCallback( + (column: string, type: '+' | '-') => { + const field = indexPattern.fields.getByName(column); + filter(field!, flattenedRow[column], type); + }, + [filter, flattenedRow, indexPattern.fields] + ); + + const getContextAppHref = () => { + return getContextUrl(row._id, indexPattern.id!, columns, filterManager, addBasePath); + }; + + const getSingleDocHref = () => { + return addBasePath(getSingleDocUrl(indexPattern.id!, row._index, row._id)); + }; + + const rowCells = [ + + + {open ? ( + + ) : ( + + )} + + , + ]; + + if (indexPattern.timeFieldName && !hideTimeColumn) { + rowCells.push( + + ); + } + + if (columns.length === 0 && useNewFieldsApi) { + const formatted = formatRow(row, indexPattern); + + rowCells.push( + + ); + } else { + columns.forEach(function (column: string) { + // when useNewFieldsApi is true, addressing to the fields property is safe + if (useNewFieldsApi && !mapping(column) && !row.fields![column]) { + const innerColumns = Object.fromEntries( + Object.entries(row.fields!).filter(([key]) => { + return key.indexOf(`${column}.`) === 0; + }) + ); + + rowCells.push( + + ); + } else { + const isFilterable = Boolean(mapping(column)?.filterable && filter); + rowCells.push( + + ); + } + }); + } + + return ( + + + {rowCells} + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap new file mode 100644 index 000000000000..5f3564174adf --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Doc table cell component renders a cell with filter buttons if it is filterable 1`] = ` + + formatted content +
+ } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + + + + + + + + + + + + + + +`; + +exports[`Doc table cell component renders a cell without filter buttons if it is not filterable 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a field that is neither a timefield or sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={false} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={true} + timefield={false} +> + + + formatted content + + + + +`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss similarity index 64% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss index e175a2f3383e..2e643c195208 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss @@ -3,7 +3,15 @@ } .kbnDocTableCell__toggleDetails { - padding: $euiSizeXS 0 0 0!important; + padding: $euiSizeXS 0 0 0 !important; +} + +/** + * Fixes time column width in Firefox after toggle display of the rows details. + * Described issue - https://github.com/elastic/kibana/pull/104361#issuecomment-894271241 + */ +.kbnDocTableCell--extraWidth { + width: 1%; } .kbnDocTableCell__filter { @@ -12,6 +20,11 @@ right: 0; } +.kbnDocTableCell__filterButton { + font-size: $euiFontSizeXS; + padding: $euiSizeXS; +} + /** * 1. Align icon with text in cell. * 2. Use opacity to make this element accessible to screen readers and keyboard. diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx new file mode 100644 index 000000000000..316c2b27357a --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.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 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 React from 'react'; +import { mount } from 'enzyme'; +import { CellProps, TableCell } from './table_cell'; + +const mountComponent = (props: Omit) => { + return mount( {}} />); +}; + +describe('Doc table cell component', () => { + test('renders a cell without filter buttons if it is not filterable', () => { + const component = mountComponent({ + filterable: false, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }); + expect(component).toMatchSnapshot(); + }); + + it('renders a cell with filter buttons if it is filterable', () => { + expect( + mountComponent({ + filterable: true, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: true, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a field that is neither a timefield or sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx new file mode 100644 index 000000000000..ad2368439d6d --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import classNames from 'classnames'; +import { TableCellActions } from './table_cell_actions'; +export interface CellProps { + timefield: boolean; + sourcefield?: boolean; + formatted: JSX.Element; + filterable: boolean; + column: string; + inlineFilter: (column: string, type: '+' | '-') => void; +} + +export const TableCell = (props: CellProps) => { + const classes = classNames({ + ['eui-textNoWrap kbnDocTableCell--extraWidth']: props.timefield, + ['eui-textBreakAll eui-textBreakWord']: props.sourcefield, + ['kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord']: + !props.timefield && !props.sourcefield, + }); + + const handleFilterFor = () => props.inlineFilter(props.column, '+'); + const handleFilterOut = () => props.inlineFilter(props.column, '-'); + + return ( + + {props.formatted} + {props.filterable ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx new file mode 100644 index 000000000000..f252c8d80139 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface TableCellActionsProps { + handleFilterFor: () => void; + handleFilterOut: () => void; +} + +export const TableCellActions = ({ handleFilterFor, handleFilterOut }: TableCellActionsProps) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx new file mode 100644 index 000000000000..c3ff53fe2d3a --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface TableRowDetailsProps { + open: boolean; + colLength: number; + isTimeBased: boolean; + getContextAppHref: () => string; + getSingleDocHref: () => string; + children: JSX.Element; +} + +export const TableRowDetails = ({ + open, + colLength, + isTimeBased, + getContextAppHref, + getSingleDocHref, + children, +}: TableRowDetailsProps) => { + if (!open) { + return null; + } + + return ( + + + + + + + + + +

+ +

+
+
+
+
+ + + + {isTimeBased && ( + + + + )} + + + + + + + + +
+
{children}
+ + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx new file mode 100644 index 000000000000..c745fbf64d29 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.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 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 React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { DocTableEmbeddable, DocTableEmbeddableProps } from './doc_table_embeddable'; + +export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) { + return ( + + + + ); +} diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx new file mode 100644 index 000000000000..8d29efec7371 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx @@ -0,0 +1,33 @@ +/* + * 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 React, { Fragment } from 'react'; +import './index.scss'; +import { SkipBottomButton } from '../skip_bottom_button'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; + +const DocTableWrapperMemoized = React.memo(DocTableWrapper); + +const renderDocTable = (tableProps: DocTableRenderProps) => { + return ( + + + + {tableProps.renderHeader()} + {tableProps.renderRows(tableProps.rows)} +
+ + ​ + +
+ ); +}; + +export const DocTableContext = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx new file mode 100644 index 000000000000..04902af692b7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React, { memo, useCallback, useMemo } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { SAMPLE_SIZE_SETTING } from '../../../../../../common'; +import { usePager } from './lib/use_pager'; +import { ToolBarPagination } from './components/pager/tool_bar_pagination'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { TotalDocuments } from '../total_documents/total_documents'; +import { getServices } from '../../../../../kibana_services'; + +export interface DocTableEmbeddableProps extends DocTableProps { + totalHitCount: number; +} + +const DocTableWrapperMemoized = memo(DocTableWrapper); + +export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { + const pager = usePager({ totalItems: props.rows.length }); + + const pageOfItems = useMemo( + () => props.rows.slice(pager.startIndex, pager.pageSize + pager.startIndex), + [pager.pageSize, pager.startIndex, props.rows] + ); + + const shouldShowLimitedResultsWarning = () => + !pager.hasNextPage && props.rows.length < props.totalHitCount; + + const scrollTop = () => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + }; + + const onPageChange = (page: number) => { + scrollTop(); + pager.onPageChange(page); + }; + + const onPageSizeChange = (size: number) => { + scrollTop(); + pager.onPageSizeChange(size); + }; + + const sampleSize = useMemo(() => { + return getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500); + }, []); + + const renderDocTable = useCallback( + (renderProps: DocTableRenderProps) => { + return ( +
+ + {renderProps.renderHeader()} + {renderProps.renderRows(pageOfItems)} +
+
+ ); + }, + [pageOfItems] + ); + + return ( + + + + {shouldShowLimitedResultsWarning() && ( + + + + + + )} + {props.totalHitCount !== 0 && ( + + + + )} + + + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx new file mode 100644 index 000000000000..8e9066151b36 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.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 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 React, { Fragment, memo, useCallback, useEffect, useState } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { debounce } from 'lodash'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { SkipBottomButton } from '../skip_bottom_button'; + +const DocTableInfiniteContent = (props: DocTableRenderProps) => { + const [limit, setLimit] = useState(props.minimumVisibleRows); + + // Reset infinite scroll limit + useEffect(() => { + setLimit(props.minimumVisibleRows); + }, [props.rows, props.minimumVisibleRows]); + + /** + * depending on which version of Discover is displayed, different elements are scrolling + * and have therefore to be considered for calculation of infinite scrolling + */ + useEffect(() => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + const scrollMobileElem = document.documentElement; + + const scheduleCheck = debounce(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv; + + const scrollusedHeight = usedScrollDiv.scrollHeight; + const scrollTop = Math.abs(usedScrollDiv.scrollTop); + const clientHeight = usedScrollDiv.clientHeight; + + if (scrollTop + clientHeight === scrollusedHeight) { + setLimit((prevLimit) => prevLimit + 50); + } + }, 50); + + scrollDiv.addEventListener('scroll', scheduleCheck); + window.addEventListener('scroll', scheduleCheck); + + scheduleCheck(); + + return () => { + scrollDiv.removeEventListener('scroll', scheduleCheck); + window.removeEventListener('scroll', scheduleCheck); + }; + }, []); + + const onBackToTop = useCallback(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const focusElem = document.querySelector('.dscTable') as HTMLElement; + focusElem.focus(); + + // Only the desktop one needs to target a specific container + if (!isMobileView) { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, []); + + return ( + + + + {props.renderHeader()} + {props.renderRows(props.rows.slice(0, limit))} +
+ {props.rows.length === props.sampleSize ? ( +
+ + + + +
+ ) : ( + + ​ + + )} +
+ ); +}; + +const DocTableWrapperMemoized = memo(DocTableWrapper); +const DocTableInfiniteContentMemoized = memo(DocTableInfiniteContent); + +const renderDocTable = (tableProps: DocTableRenderProps) => ( + +); + +export const DocTableInfinite = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx new file mode 100644 index 000000000000..df5869bd61e5 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import { findTestSubject, mountWithIntl } from '@kbn/test/jest'; +import { setServices } from '../../../../../kibana_services'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { DocTableWrapper, DocTableWrapperProps } from './doc_table_wrapper'; +import { DocTableRow } from './components/table_row'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; + +const mountComponent = (props: DocTableWrapperProps) => { + return mountWithIntl(); +}; + +describe('Doc table component', () => { + let defaultProps: DocTableWrapperProps; + + const initDefaults = (rows?: DocTableRow[]) => { + defaultProps = { + columns: ['_source'], + indexPattern: indexPatternMock, + rows: rows || [ + { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, + }, + ], + sort: [['order_date', 'desc']], + isLoading: false, + searchDescription: '', + onAddColumn: () => {}, + onFilter: () => {}, + onMoveColumn: () => {}, + onRemoveColumn: () => {}, + onSort: () => {}, + useNewFieldsApi: true, + dataTestSubj: 'discoverDocTable', + render: () => { + return
mock
; + }, + }; + + setServices(discoverServiceMock); + }; + + it('should render infinite table correctly', () => { + initDefaults(); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeTruthy(); + expect(component.find('.kbnDocTable__error').exists()).toBeFalsy(); + }); + + it('should render error fallback if rows array is empty', () => { + initDefaults([]); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeFalsy(); + expect(component.find('.kbnDocTable__error').exists()).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx new file mode 100644 index 000000000000..c875bf155bd7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx @@ -0,0 +1,243 @@ +/* + * 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 React, { useCallback, useMemo, useState } from 'react'; +import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TableHeader } from './components/table_header/table_header'; +import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SAMPLE_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../kibana_services'; +import { SortOrder } from './components/table_header/helpers'; +import { DocTableRow, TableRow } from './components/table_row'; +import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; + +export interface DocTableProps { + /** + * Rows of classic table + */ + rows: DocTableRow[]; + /** + * Columns of classic table + */ + columns: string[]; + /** + * Current IndexPattern + */ + indexPattern: IndexPattern; + /** + * Current sorting + */ + sort: string[][]; + /** + * New fields api switch + */ + useNewFieldsApi: boolean; + /** + * Current search description + */ + searchDescription?: string; + /** + * Current shared item title + */ + sharedItemTitle?: string; + /** + * Current data test subject + */ + dataTestSubj: string; + /** + * Loading state + */ + isLoading: boolean; + /** + * Filter callback + */ + onFilter: DocViewFilterFn; + /** + * Sorting callback + */ + onSort?: (sort: string[][]) => void; + /** + * Add columns callback + */ + onAddColumn?: (column: string) => void; + /** + * Reordering column callback + */ + onMoveColumn?: (columns: string, newIdx: number) => void; + /** + * Remove column callback + */ + onRemoveColumn?: (column: string) => void; +} + +export interface DocTableRenderProps { + rows: DocTableRow[]; + minimumVisibleRows: number; + sampleSize: number; + renderRows: (row: DocTableRow[]) => JSX.Element[]; + renderHeader: () => JSX.Element; + onSkipBottomButtonClick: () => void; +} + +export interface DocTableWrapperProps extends DocTableProps { + /** + * Renders Doc table content + */ + render: (params: DocTableRenderProps) => JSX.Element; +} + +export const DocTableWrapper = ({ + render, + columns, + rows, + indexPattern, + onSort, + onAddColumn, + onMoveColumn, + onRemoveColumn, + sort, + onFilter, + useNewFieldsApi, + searchDescription, + sharedItemTitle, + dataTestSubj, + isLoading, +}: DocTableWrapperProps) => { + const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); + const [ + defaultSortOrder, + hideTimeColumn, + isShortDots, + sampleSize, + filterManager, + addBasePath, + ] = useMemo(() => { + const services = getServices(); + return [ + services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), + services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + services.uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), + services.uiSettings.get(SAMPLE_SIZE_SETTING, 500), + services.filterManager, + services.addBasePath, + ]; + }, []); + + const onSkipBottomButtonClick = useCallback(async () => { + // delay scrolling to after the rows have been rendered + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + // show all the rows + setMinimumVisibleRows(rows.length); + + while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker!.focus(); + await wait(50); + bottomMarker!.blur(); + }, [setMinimumVisibleRows, rows]); + + const renderHeader = useCallback( + () => ( + + ), + [ + columns, + defaultSortOrder, + hideTimeColumn, + indexPattern, + isShortDots, + onMoveColumn, + onRemoveColumn, + onSort, + sort, + ] + ); + + const renderRows = useCallback( + (rowsToRender: DocTableRow[]) => { + return rowsToRender.map((current) => ( + + )); + }, + [ + columns, + onFilter, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, + ] + ); + + return ( +
+ {rows.length !== 0 && + render({ + rows, + minimumVisibleRows, + sampleSize, + onSkipBottomButtonClick, + renderHeader, + renderRows, + })} + {!rows.length && ( +
+ + + + + +
+ )} +
+ ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts similarity index 90% rename from src/plugins/discover/public/application/angular/doc_table/index.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.ts index 3a8f170f8680..513183cc9946 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { createDocTableDirective } from './doc_table'; export { getSort, getSortArray } from './lib/get_sort'; export { getSortForSearchSource } from './lib/get_sort_for_search_source'; export { getDefaultSort } from './lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts similarity index 91% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts index f181d583f021..b2c7499b4a04 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts @@ -8,8 +8,8 @@ import { getDefaultSort } from './get_default_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('getDefaultSort function', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts similarity index 93% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts index aa1cf4a61066..e01ff0b00e2b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../kibana_services'; +import { IndexPattern } from '../../../../../../kibana_services'; import { isSortable } from './get_sort'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts index 19d629e14da6..865ef1d3fb72 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts @@ -8,8 +8,8 @@ import { getSort, getSortArray } from './get_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('docTable', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts index 4b16c1aa3dcc..2c687a59ea29 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { IndexPattern } from '../../../../../../data/public'; +import { IndexPattern } from '../../../../../../../../data/public'; export type SortPairObj = Record; export type SortPairArr = [string, string]; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts similarity index 94% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts index dc7817d95dd3..3753597ced16 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts @@ -8,8 +8,8 @@ import { getSortForSearchSource } from './get_sort_for_search_source'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; describe('getSortForSearchSource function', function () { diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts similarity index 95% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts index 58a690b70529..2bc8a71301df 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, IndexPattern } from '../../../../kibana_services'; +import { EsQuerySortValue, IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; import { getSort } from './get_sort'; diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts similarity index 53% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts index 6b356446850e..8c108e7d4dcf 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ +import ReactDOM from 'react-dom/server'; import { formatRow, formatTopLevelObject } from './row_formatter'; -import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; -import { fieldFormatsMock } from '../../../../../field_formats/common/mocks'; -import { setServices } from '../../../kibana_services'; -import { DiscoverServices } from '../../../build_services'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../__mocks__/stubbed_saved_object_index_pattern'; +import { IndexPattern } from '../../../../../../../../data/common/index_patterns/index_patterns'; +import { fieldFormatsMock } from '../../../../../../../../field_formats/common/mocks'; +import { setServices } from '../../../../../../kibana_services'; +import { DiscoverServices } from '../../../../../../build_services'; describe('Row formatter', () => { const hit = { @@ -68,9 +69,42 @@ describe('Row formatter', () => { }); it('formats document properly', () => { - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
number:
42
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('limits number of rendered items', () => { @@ -79,17 +113,57 @@ describe('Row formatter', () => { get: () => 1, }, } as unknown) as DiscoverServices); - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats document with highlighted fields first', () => { - expect( - formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() - ).toMatchInlineSnapshot( - `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow({ ...hit, highlight: { number: '42' } }, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats top level objects using formatter', () => { @@ -111,10 +185,19 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects in alphabetical order', () => { @@ -124,11 +207,13 @@ describe('Row formatter', () => { indexPattern.getFormatterForField = jest.fn().mockReturnValue({ convert: () => 'formatted', }); - const formatted = formatTopLevelObject( - { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, - { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, - indexPattern - ).trim(); + const formatted = ReactDOM.renderToStaticMarkup( + formatTopLevelObject( + { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, + { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, + indexPattern + ) + ); expect(formatted.indexOf('
a.ccc:
')).toBeLessThan(formatted.indexOf('
a.zzz:
')); }); @@ -156,10 +241,23 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.keys:
formatted, formatted
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects, converting unknown fields to string', () => { @@ -177,9 +275,18 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
5, 10
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); }); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx similarity index 87% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index c410273cc751..51e83f78f9f1 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -7,9 +7,8 @@ */ import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom/server'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; -import { getServices, IndexPattern } from '../../../kibana_services'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../../kibana_services'; interface Props { defPairs: Array<[string, unknown]>; @@ -44,9 +43,7 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) pairs.push([displayKey ? displayKey : key, val]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; export const formatTopLevelObject = ( @@ -80,7 +77,5 @@ export const formatTopLevelObject = ( pairs.push([displayKey ? displayKey : key, formatted]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts new file mode 100644 index 000000000000..5522e3c15021 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts @@ -0,0 +1,78 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +interface MetaParams { + currentPage: number; + totalItems: number; + totalPages: number; + startIndex: number; + hasNextPage: boolean; + pageSize: number; +} + +interface ProvidedMeta { + updatedPageSize?: number; + updatedCurrentPage?: number; +} + +const INITIAL_PAGE_SIZE = 50; + +export const usePager = ({ totalItems }: { totalItems: number }) => { + const [meta, setMeta] = useState({ + currentPage: 0, + totalItems, + startIndex: 0, + totalPages: Math.ceil(totalItems / INITIAL_PAGE_SIZE), + hasNextPage: true, + pageSize: INITIAL_PAGE_SIZE, + }); + + const getNewMeta = useCallback( + (newMeta: ProvidedMeta) => { + const actualCurrentPage = newMeta.updatedCurrentPage ?? meta.currentPage; + const actualPageSize = newMeta.updatedPageSize ?? meta.pageSize; + + const newTotalPages = Math.ceil(totalItems / actualPageSize); + const newStartIndex = actualPageSize * actualCurrentPage; + + return { + currentPage: actualCurrentPage, + totalPages: newTotalPages, + startIndex: newStartIndex, + totalItems, + hasNextPage: meta.currentPage + 1 < meta.totalPages, + pageSize: actualPageSize, + }; + }, + [meta.currentPage, meta.pageSize, meta.totalPages, totalItems] + ); + + const onPageChange = useCallback( + (pageIndex: number) => setMeta(getNewMeta({ updatedCurrentPage: pageIndex })), + [getNewMeta] + ); + + const onPageSizeChange = useCallback( + (newPageSize: number) => + setMeta(getNewMeta({ updatedPageSize: newPageSize, updatedCurrentPage: 0 })), + [getNewMeta] + ); + + /** + * Update meta on totalItems change + */ + useEffect(() => setMeta(getNewMeta({})), [getNewMeta, totalItems]); + + return { + ...meta, + onPageChange, + onPageSizeChange, + }; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx index 1136b693c9e7..e5212e877e8b 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx @@ -36,7 +36,6 @@ function getProps(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { return { expandedDoc: undefined, indexPattern: indexPatternMock, - isMobile: jest.fn(() => false), onAddFilter: jest.fn(), savedSearch: savedSearchMock, documents$, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx index 13cf021ff257..e0e0c9c6f883 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx @@ -5,11 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useRef, useMemo, useCallback, memo } from 'react'; -import { EuiFlexItem, EuiSpacer, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useMemo, useCallback, memo } from 'react'; +import { + EuiFlexItem, + EuiSpacer, + EuiText, + EuiLoadingSpinner, + EuiScreenReaderOnly, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react'; -import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; import { FetchStatus } from '../../../../types'; @@ -26,15 +30,16 @@ import { DataDocumentsMsg, DataDocuments$ } from '../../services/use_saved_searc import { DiscoverServices } from '../../../../../build_services'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { useDataState } from '../../utils/use_data_state'; +import { DocTableInfinite } from '../doc_table/doc_table_infinite'; +import { SortPairArr } from '../doc_table/lib/get_sort'; -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); function DiscoverDocumentsComponent({ documents$, expandedDoc, indexPattern, - isMobile, onAddFilter, savedSearch, services, @@ -45,7 +50,6 @@ function DiscoverDocumentsComponent({ documents$: DataDocuments$; expandedDoc?: ElasticSearchHit; indexPattern: IndexPattern; - isMobile: () => boolean; navigateTo: (url: string) => void; onAddFilter: DocViewFilterFn; savedSearch: SavedSearch; @@ -57,11 +61,11 @@ function DiscoverDocumentsComponent({ const { capabilities, indexPatterns, uiSettings } = services; const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); - const scrollableDesktop = useRef(null); const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); const documentState: DataDocumentsMsg = useDataState(documents$); + const isLoading = documentState.fetchStatus === FetchStatus.LOADING; const rows = useMemo(() => documentState.result || [], [documentState.result]); @@ -75,21 +79,6 @@ function DiscoverDocumentsComponent({ useNewFieldsApi, }); - /** - * Legacy function, remove once legacy grid is removed - */ - const onBackToTop = useCallback(() => { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }, [scrollableDesktop, isMobile]); - const onResize = useCallback( (colSettings: { columnId: string; width: number }) => { const grid = { ...state.grid } || {}; @@ -131,62 +120,57 @@ function DiscoverDocumentsComponent({ } return ( - -
-

+ + +

- {isLegacy && rows && rows.length && ( - + {isLegacy && rows && rows.length && ( + + )} + {!isLegacy && ( +
+ - )} - {!isLegacy && ( -
- -
- )} -

+
+ )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss index b3a4453425ac..2401325dd76f 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss @@ -78,30 +78,14 @@ discover-app { } .dscHistogram { - display: flex; - height: $euiSize * 12.5; - padding: $euiSizeS; -} - -// new slimmer layout for data grid -.dscHistogramGrid { display: flex; height: $euiSize * 8; padding: $euiSizeS $euiSizeS 0 $euiSizeS; } .dscTable { - // SASSTODO: add a monospace modifier to the doc-table component - .kbnDocTable__row { - font-family: $euiCodeFontFamily; - font-size: $euiFontSizeXS; - } -} - -.dscTable__footer { - background-color: $euiColorLightShade; - padding: $euiSizeXS $euiSizeS; - text-align: center; + // needs for scroll container of lagacy table + min-height: 0; } .dscDocuments__loading { diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 785cde6fd311..94e28c3f1d54 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover_layout.scss'; -import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { EuiSpacer, EuiButtonIcon, @@ -31,7 +31,7 @@ import { } from '../../../../../../../data/public'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; -import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common'; import { popularizeField } from '../../../../helpers/popularize_field'; import { DiscoverTopNav } from '../top_nav/discover_topnav'; import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; @@ -66,13 +66,11 @@ export function DiscoverLayout({ stateContainer, }: DiscoverLayoutProps) { const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services; + const { main$, charts$, totalHits$ } = savedSearchData$; const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); - const collapseIcon = useRef(null); const fetchCounter = useRef(0); - const { main$, charts$, totalHits$ } = savedSearchData$; - const dataState: DataMainMsg = useDataState(main$); useEffect(() => { @@ -81,14 +79,11 @@ export function DiscoverLayout({ } }, [dataState.fetchStatus]); - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - const isMobile = useCallback(() => collapseIcon && !collapseIcon.current, []); const timeField = useMemo(() => { return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const resultState = useMemo( @@ -209,7 +204,6 @@ export function DiscoverLayout({ aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { defaultMessage: 'Toggle sidebar', })} - buttonRef={collapseIcon} />
@@ -251,7 +245,6 @@ export function DiscoverLayout({ > + { + return ( + + {totalHitCount}, + }} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index 1bf9710def03..a5a064a8fc1c 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -24,7 +24,7 @@ import { import { useSearchSession } from './use_search_session'; import { FetchStatus } from '../../../types'; import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; +import { SortPairArr } from '../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 00473956c57e..225d90c61de1 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -10,8 +10,8 @@ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; import { ISearchSource } from '../../../../../../data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; +import { getSortForSearchSource } from '../components/doc_table'; import { AppState } from '../services/discover_state'; -import { getSortForSearchSource } from '../../../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 426272fa8ce1..fc835d4d3dd1 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -9,12 +9,11 @@ import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import { getSortArray } from '../../../angular/doc_table'; -import { getDefaultSort } from '../../../angular/doc_table/lib/get_default_sort'; import { SavedSearch } from '../../../../saved_searches'; import { DataPublicPluginStart } from '../../../../../../data/public'; import { AppState } from '../services/discover_state'; +import { getDefaultSort, getSortArray } from '../components/doc_table'; function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts index f7154b26c7ed..00f194662e41 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { getSortArray } from '../../../angular/doc_table'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; import { IndexPattern } from '../../../../kibana_services'; +import { getSortArray, SortPairArr } from '../components/doc_table/lib/get_sort'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 3fac75a198d5..b4a1dab41a09 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { getSortForSearchSource } from '../../../angular/doc_table'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; import { SortOrder } from '../../../../saved_searches/types'; import { DiscoverServices } from '../../../../build_services'; import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; +import { getSortForSearchSource } from '../components/doc_table'; /** * Helper function to update the given searchSource before fetching/sharing/persisting diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index c52f22c60bb5..37963eb2dfa9 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -25,6 +25,7 @@ import { useContextAppFetch } from './use_context_app_fetch'; import { popularizeField } from '../../helpers/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from '../../angular/context/api/context'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -161,7 +162,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} setAppState={setAppState} - addFilter={addFilter} + addFilter={addFilter as DocViewFilterFn} rows={rows} predecessors={fetchedState.predecessors} successors={fetchedState.successors} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx index 0536ab7e6a02..1b95af8bdbe1 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx @@ -8,73 +8,76 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; -import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { AppState, GetStateReturn } from '../../angular/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { EsHitRecordList } from '../../angular/context/api/context'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; -import { getServices } from '../../../kibana_services'; +import { getServices, setServices } from '../../../kibana_services'; import { LoadingStatus } from '../../angular/context_query_state'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { DiscoverGrid } from '../discover_grid/discover_grid'; - -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - uiSettings: mockUiSettings, - }), - }; -}); +import { discoverServiceMock } from '../../../__mocks__/services'; +import { DocTableWrapper } from '../../apps/main/components/doc_table/doc_table_wrapper'; describe('ContextAppContent test', () => { - const hit = { - _id: '123', - _index: 'test_index', - _score: null, - _version: 1, - _source: { - category: ["Men's Clothing"], - currency: 'EUR', - customer_first_name: 'Walker', - customer_full_name: 'Walker Texas Ranger', - customer_gender: 'MALE', - customer_last_name: 'Ranger', - }, - fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], - sort: [1603114502000, 2092], - }; - const defaultProps = ({ - columns: ['Time (@timestamp)', '_source'], - indexPattern: indexPatternMock, - appState: ({} as unknown) as AppState, - stateContainer: ({} as unknown) as GetStateReturn, - anchorStatus: LoadingStatus.LOADED, - predecessorsStatus: LoadingStatus.LOADED, - successorsStatus: LoadingStatus.LOADED, - rows: ([hit] as unknown) as EsHitRecordList, - predecessors: [], - successors: [], - defaultStepSize: 5, - predecessorCount: 10, - successorCount: 10, - useNewFieldsApi: false, - isPaginationEnabled: false, - onAddColumn: () => {}, - onRemoveColumn: () => {}, - onSetColumns: () => {}, - services: getServices(), - sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, - isLegacy: true, - setAppState: () => {}, - addFilter: () => {}, - } as unknown) as ContextAppContentProps; + let hit; + let defaultProps: ContextAppContentProps; + + beforeEach(() => { + setServices(discoverServiceMock); + + hit = { + _id: '123', + _index: 'test_index', + _score: null, + _version: 1, + fields: [ + { + order_date: ['2020-10-19T13:35:02.000Z'], + }, + ], + _source: { + category: ["Men's Clothing"], + currency: 'EUR', + customer_first_name: 'Walker', + customer_full_name: 'Walker Texas Ranger', + customer_gender: 'MALE', + customer_last_name: 'Ranger', + }, + sort: [1603114502000, 2092], + }; + defaultProps = ({ + columns: ['order_date', '_source'], + indexPattern: indexPatternMock, + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorStatus: LoadingStatus.LOADED, + predecessorsStatus: LoadingStatus.LOADED, + successorsStatus: LoadingStatus.LOADED, + rows: ([hit] as unknown) as EsHitRecordList, + predecessors: [], + successors: [], + defaultStepSize: 5, + predecessorCount: 10, + successorCount: 10, + useNewFieldsApi: true, + isPaginationEnabled: false, + onAddColumn: () => {}, + onRemoveColumn: () => {}, + onSetColumns: () => {}, + services: getServices(), + sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, + isLegacy: true, + setAppState: () => {}, + addFilter: () => {}, + } as unknown) as ContextAppContentProps; + }); it('should render legacy table correctly', () => { const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(1); + expect(component.find(DocTableWrapper).length).toBe(1); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); expect(component.find(ActionBar).length).toBe(2); @@ -84,18 +87,11 @@ describe('ContextAppContent test', () => { const props = { ...defaultProps }; props.anchorStatus = LoadingStatus.LOADING; const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(component.find(DocTableWrapper).length).toBe(1); expect(loadingIndicator.length).toBe(1); }); - it('renders error message', () => { - const props = { ...defaultProps }; - props.anchorStatus = LoadingStatus.FAILED; - const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); - }); - it('should render discover grid correctly', () => { const props = { ...defaultProps, isLegacy: false }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 4d7ce2aa5209..78c354cbf908 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -10,20 +10,17 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common'; +import { IndexPattern } from '../../../../../data/common'; import { SortDirection } from '../../../../../data/public'; -import { - DocTableLegacy, - DocTableLegacyProps, -} from '../../angular/doc_table/create_doc_table_react'; import { LoadingStatus } from '../../angular/context_query_state'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverGrid } from '../discover_grid/discover_grid'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; -import { EsHitRecord, EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; +import { EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; +import { DocTableContext } from '../../apps/main/components/doc_table/doc_table_context'; export interface ContextAppContentProps { columns: string[]; @@ -44,11 +41,7 @@ export interface ContextAppContentProps { useNewFieldsApi: boolean; isLegacy: boolean; setAppState: (newState: Partial) => void; - addFilter: ( - field: IndexPatternField | string, - values: unknown, - operation: string - ) => Promise; + addFilter: DocViewFilterFn; } const controlColumnIds = ['openDetails']; @@ -57,8 +50,8 @@ export function clamp(value: number) { return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); } -const DataGridMemoized = React.memo(DiscoverGrid); -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DiscoverGridMemoized = React.memo(DiscoverGrid); +const DocTableContextMemoized = React.memo(DocTableContext); const ActionBarMemoized = React.memo(ActionBar); export function ContextAppContent({ @@ -84,8 +77,7 @@ export function ContextAppContent({ }: ContextAppContentProps) { const { uiSettings: config } = services; - const [expandedDoc, setExpandedDoc] = useState(undefined); - const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const [expandedDoc, setExpandedDoc] = useState(); const isAnchorLoading = anchorStatus === LoadingStatus.LOADING || anchorStatus === LoadingStatus.UNINITIALIZED; const arePredecessorsLoading = @@ -100,50 +92,8 @@ export function ContextAppContent({ ); const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]); - const docTableProps = () => { - return { - ariaLabelledBy: 'surDocumentsAriaLabel', - columns, - rows: rows as ElasticSearchHit[], - indexPattern, - expandedDoc, - isLoading: isAnchorLoading, - sampleSize: 0, - sort: sort as [[string, SortDirection]], - isSortEnabled: false, - showTimeCol, - services, - useNewFieldsApi, - isPaginationEnabled: false, - controlColumnIds, - setExpandedDoc, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - onSetColumns, - } as DiscoverGridProps; - }; - - const legacyDocTableProps = () => { - // @ts-expect-error doesn't implement full DocTableLegacyProps interface - return { - columns, - indexPattern, - minimumVisibleRows: rows.length, - rows, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - sort, - useNewFieldsApi, - } as DocTableLegacyProps; - }; - const loadingFeedback = () => { - if ( - isLegacy && - (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) - ) { + if (isLegacy && isAnchorLoading) { return ( @@ -170,18 +120,47 @@ export function ContextAppContent({ docCountAvailable={predecessors.length} onChangeCount={onChangeCount} isLoading={arePredecessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> {loadingFeedback()} - {isLegacy && isAnchorLoaded && ( -
- -
+ {isLegacy && rows && rows.length !== 0 && ( + )} - {!isLegacy && ( + {!isLegacy && rows && rows.length && (
- +
)} @@ -192,7 +171,7 @@ export function ContextAppContent({ docCountAvailable={successors.length} onChangeCount={onChangeCount} isLoading={areSuccessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index f1c56b7a5719..c727e7784cca 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -28,7 +28,6 @@ import { DiscoverGridFlyout } from './discover_grid_flyout'; import { DiscoverGridContext } from './discover_grid_context'; import { getRenderCellValueFn } from './get_render_cell_value'; import { DiscoverGridSettings } from './types'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; import { getEuiGridColumns, getLeadControlColumns, @@ -40,6 +39,7 @@ import { getDisplayedColumns } from '../../helpers/columns'; import { KibanaContextProvider } from '../../../../../kibana_react/public'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; +import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; interface SortObj { id: string; @@ -369,7 +369,7 @@ export const DiscoverGrid = ({ > {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index 12d56d564b85..e845ba723830 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -45,7 +45,6 @@ } .kbnDocViewer__value { - display: inline-block; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; diff --git a/src/plugins/discover/public/application/components/table/table_helper.test.ts b/src/plugins/discover/public/application/components/table/table_helper.test.ts deleted file mode 100644 index 738556aaea08..000000000000 --- a/src/plugins/discover/public/application/components/table/table_helper.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { arrayContainsObjects } from './table_helper'; - -describe('arrayContainsObjects', () => { - it(`returns false for an array of primitives`, () => { - const actual = arrayContainsObjects(['test', 'test']); - expect(actual).toBeFalsy(); - }); - - it(`returns true for an array of objects`, () => { - const actual = arrayContainsObjects([{}, {}]); - expect(actual).toBeTruthy(); - }); - - it(`returns true for an array of objects and primitves`, () => { - const actual = arrayContainsObjects([{}, 'sdf']); - expect(actual).toBeTruthy(); - }); - - it(`returns false for an array of null values`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); - - it(`returns false if no array is given`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); -}); diff --git a/src/plugins/discover/public/application/components/table/table_helper.tsx b/src/plugins/discover/public/application/components/table/table_helper.tsx index 6af349af11f1..e1c3de8d87c3 100644 --- a/src/plugins/discover/public/application/components/table/table_helper.tsx +++ b/src/plugins/discover/public/application/components/table/table_helper.tsx @@ -6,13 +6,6 @@ * Side Public License, v 1. */ -/** - * Returns true if the given array contains at least 1 object - */ -export function arrayContainsObjects(value: unknown[]): boolean { - return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null); -} - /** * Removes markup added by kibana fields html formatter */ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 58399f31e032..fe185d7c21f0 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,7 +18,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; +export type ElasticSearchHit = estypes.SearchHit; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index 098c7f55fbd9..3fd7b2f50d31 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -22,11 +22,10 @@ import { Query, TimeRange, Filter, - IndexPatternField, IndexPattern, ISearchSource, + IndexPatternField, } from '../../../../data/common'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; import { UiActionsStart } from '../../../../ui_actions/public'; @@ -38,23 +37,26 @@ import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; -import * as columnActions from '../angular/doc_table/actions/columns'; -import { getSortForSearchSource, getDefaultSort } from '../angular/doc_table'; +import * as columnActions from '../apps/main/components/doc_table/actions/columns'; import { handleSourceColumnState } from '../angular/helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; - -export interface SearchProps extends Partial { - settings?: DiscoverGridSettings; - description?: string; - sharedItemTitle?: string; - inspectorAdapters?: Adapters; - - filter?: (field: IndexPatternField, value: string[], operator: string) => void; - hits?: ElasticSearchHit[]; - totalHitCount?: number; - onMoveColumn?: (column: string, index: number) => void; -} +import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapper'; +import { getDefaultSort, getSortForSearchSource } from '../apps/main/components/doc_table'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; + +export type SearchProps = Partial & + Partial & { + settings?: DiscoverGridSettings; + description?: string; + sharedItemTitle?: string; + inspectorAdapters?: Adapters; + + filter?: (field: IndexPatternField, value: string[], operator: string) => void; + hits?: ElasticSearchHit[]; + totalHitCount?: number; + onMoveColumn?: (column: string, index: number) => void; + }; interface SearchEmbeddableConfig { savedSearch: SavedSearch; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx index 5b2a2635d04b..76b316d575cf 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -8,9 +8,12 @@ import React from 'react'; -import { DiscoverGridEmbeddable } from '../angular/create_discover_grid_directive'; -import { DiscoverDocTableEmbeddable } from '../angular/doc_table/create_doc_table_embeddable'; -import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; +import { + DiscoverGridEmbeddable, + DiscoverGridEmbeddableProps, +} from '../angular/create_discover_grid_directive'; +import { DiscoverDocTableEmbeddable } from '../apps/main/components/doc_table/create_doc_table_embeddable'; +import { DocTableEmbeddableProps } from '../apps/main/components/doc_table/doc_table_embeddable'; import { SearchProps } from './saved_search_embeddable'; interface SavedSearchEmbeddableComponentProps { @@ -32,8 +35,8 @@ export function SavedSearchEmbeddableComponent({ ...searchProps, refs, }; - return ; + return ; } - const discoverGridProps = searchProps as DiscoverGridProps; + const discoverGridProps = searchProps as DiscoverGridEmbeddableProps; return ; } diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover/public/application/embeddable/types.ts index 642c65c4b2a5..5a08534918d4 100644 --- a/src/plugins/discover/public/application/embeddable/types.ts +++ b/src/plugins/discover/public/application/embeddable/types.ts @@ -12,9 +12,9 @@ import { EmbeddableOutput, IEmbeddable, } from 'src/plugins/embeddable/public'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { Filter, IndexPattern, TimeRange, Query } from '../../../../data/public'; import { SavedSearch } from '../..'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts similarity index 65% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts rename to src/plugins/discover/public/application/helpers/get_single_doc_url.ts index 7eb31459eb4f..913463e6d44a 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts +++ b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export const truncateByHeight = ({ body }: { body: string }) => { - return `
${body}
`; +export const getSingleDocUrl = (indexPatternId: string, rowIndex: string, rowId: string) => { + return `/app/discover#/doc/${indexPatternId}/${rowIndex}?id=${encodeURIComponent(rowId)}`; }; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index 418cbf6eac9c..8a28369d1f5f 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -18,7 +18,7 @@ import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, } from '../angular/context_state'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { getStateColumnActions } from '../apps/main/components/doc_table/actions/columns'; interface UseDataGridColumnsProps { capabilities: Capabilities; diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts index edbb0663d4aa..9a0ece2a434b 100644 --- a/src/plugins/discover/public/locator.test.ts +++ b/src/plugins/discover/public/locator.test.ts @@ -10,7 +10,7 @@ import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public' import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; import { FilterStateStore } from '../../data/common'; import { DiscoverAppLocatorDefinition } from './locator'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; @@ -215,7 +215,7 @@ describe('Discover url generator', () => { const { path } = await locator.getLocation({ columns: ['_source'], interval: 'auto', - sort: [['timestamp, asc']] as string[][] & SerializableState, + sort: [['timestamp, asc']] as string[][] & SerializableRecord, savedQuery: '__savedQueryId__', }); diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index fff89903bc46..bc632c7e1ccb 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import { esFilters } from '../../data/public'; @@ -14,7 +14,7 @@ import { setStateToKbnUrl } from '../../kibana_utils/public'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; -export interface DiscoverAppLocatorParams extends SerializableState { +export interface DiscoverAppLocatorParams extends SerializableRecord { /** * Optionally set saved search ID. */ @@ -33,7 +33,7 @@ export interface DiscoverAppLocatorParams extends SerializableState { /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filters. @@ -69,7 +69,7 @@ export interface DiscoverAppLocatorParams extends SerializableState { /** * Array of the used sorting [[field,direction],...] */ - sort?: string[][] & SerializableState; + sort?: string[][] & SerializableRecord; /** * id of the used saved query diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 8c71091e3ecf..b3f1ad5d0bc1 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/embeddable/common/lib/extract.ts b/src/plugins/embeddable/common/lib/extract.ts index a68c2db5ad15..3e820d54965b 100644 --- a/src/plugins/embeddable/common/lib/extract.ts +++ b/src/plugins/embeddable/common/lib/extract.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; import { extractBaseEmbeddableInput } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) => { return (state: EmbeddableStateWithType) => { @@ -30,7 +30,7 @@ export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) = if (!enhancements[key]) return; const enhancementResult = embeddables .getEnhancement(key) - .extract(enhancements[key] as SerializableState); + .extract(enhancements[key] as SerializableRecord); refs.push(...enhancementResult.references); updatedInput.enhancements![key] = enhancementResult.state; }); diff --git a/src/plugins/embeddable/common/lib/inject.ts b/src/plugins/embeddable/common/lib/inject.ts index 169ad615b9b6..6f72eb5c3721 100644 --- a/src/plugins/embeddable/common/lib/inject.ts +++ b/src/plugins/embeddable/common/lib/inject.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; import { SavedObjectReference } from '../../../../core/types'; import { injectBaseEmbeddableInput } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => { return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { @@ -27,7 +27,7 @@ export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => if (!enhancements[key]) return; updatedInput.enhancements![key] = embeddables .getEnhancement(key) - .inject(enhancements[key] as SerializableState, references); + .inject(enhancements[key] as SerializableRecord, references); }); return updatedInput; diff --git a/src/plugins/embeddable/common/lib/migrate.ts b/src/plugins/embeddable/common/lib/migrate.ts index 7dde9e1d2b2a..9323c7f56dd0 100644 --- a/src/plugins/embeddable/common/lib/migrate.ts +++ b/src/plugins/embeddable/common/lib/migrate.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract } from '../types'; import { baseEmbeddableMigrations } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; -export type MigrateFunction = (state: SerializableState, version: string) => SerializableState; +export type MigrateFunction = (state: SerializableRecord, version: string) => SerializableRecord; export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => { - const migrateFn: MigrateFunction = (state: SerializableState, version: string) => { - const enhancements = (state.enhancements as SerializableState) || {}; + const migrateFn: MigrateFunction = (state: SerializableRecord, version: string) => { + const enhancements = (state.enhancements as SerializableRecord) || {}; const factory = embeddables.getEmbeddableFactory(state.type as string); let updatedInput = baseEmbeddableMigrations[version] @@ -26,7 +26,7 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = } if (factory?.isContainerType) { - updatedInput.panels = ((state.panels as SerializableState[]) || []).map((panel) => { + updatedInput.panels = ((state.panels as SerializableRecord[]) || []).map((panel) => { return migrateFn(panel, version); }); } @@ -36,7 +36,7 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = if (!enhancements[key]) return; const enhancementDefinition = embeddables.getEnhancement(key); const migratedEnhancement = enhancementDefinition?.migrations?.[version] - ? enhancementDefinition.migrations[version](enhancements[key] as SerializableState) + ? enhancementDefinition.migrations[version](enhancements[key] as SerializableRecord) : enhancements[key]; (updatedInput.enhancements! as Record)[key] = migratedEnhancement; }); diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index a45700375672..22d8672e59a3 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import type { KibanaExecutionContext } from 'src/core/public'; +import { PersistableStateService } from '../../kibana_utils/common'; export enum ViewMode { EDIT = 'edit', @@ -28,7 +30,7 @@ export type EmbeddableInput = { /** * Reserved key for enhancements added by other plugins. */ - enhancements?: SerializableState; + enhancements?: SerializableRecord; /** * List of action IDs that this embeddable should not render. @@ -49,6 +51,8 @@ export type EmbeddableInput = { * Flag whether colors should be synced with other panels */ syncColors?: boolean; + + executionContext?: KibanaExecutionContext; }; export interface PanelState { diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index cfb16da7b46b..c644e1f3fdc2 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import { identity } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; import { getSavedObjectFinder, showSaveModal } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -39,11 +40,7 @@ import { import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; import { Storage } from '../../kibana_utils/public'; -import { - migrateToLatest, - PersistableStateService, - SerializableState, -} from '../../kibana_utils/common'; +import { migrateToLatest, PersistableStateService } from '../../kibana_utils/common'; import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service'; import { AttributeServiceOptions } from './lib/attribute_service/attribute_service'; import { EmbeddableStateWithType } from '../common/types'; @@ -240,7 +237,7 @@ export class EmbeddablePublicPlugin implements Plugin { + ((state: SerializableRecord) => { return { state, references: [] }; }), migrations: enhancement.migrations || {}, @@ -253,7 +250,7 @@ export class EmbeddablePublicPlugin implements Plugin stats, inject: identity, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return { state, references: [] }; }, migrations: {}, diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 54fe2a7b0bec..cf28f65ceaa7 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -28,6 +28,7 @@ import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { IncomingHttpHeaders } from 'http'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import { KibanaExecutionContext as KibanaExecutionContext_2 } from 'src/core/public'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { Logger } from '@kbn/logging'; @@ -53,6 +54,7 @@ import { SavedObjectAttributes } from 'kibana/server'; import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public'; import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public'; import { SchemaTypeError } from '@kbn/config-schema'; +import { SerializableRecord } from '@kbn/utility-types'; import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public'; import { Start as Start_2 } from 'src/plugins/inspector/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -318,7 +320,7 @@ export abstract class Embeddable { +export class EmbeddableChildPanel extends React.Component { constructor(props: EmbeddableChildPanelProps); // (undocumented) [panel: string]: any; @@ -416,11 +418,12 @@ export type EmbeddableInput = { id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; - enhancements?: SerializableState; + enhancements?: SerializableRecord; disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext_2; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -475,7 +478,7 @@ export interface EmbeddablePackageState { // Warning: (ae-missing-release-tag) "EmbeddablePanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class EmbeddablePanel extends React.Component { +export class EmbeddablePanel extends React.Component { constructor(props: Props); // (undocumented) closeMyContextMenuPanel: () => void; @@ -618,7 +621,7 @@ export class EmbeddableStateTransfer { // Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ +export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ // (undocumented) id: string; } @@ -895,7 +898,6 @@ export const withEmbeddableSubscription: ; export type EnhancementsRegistry = Map; -export interface EnhancementRegistryDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ id: string; } -export interface EnhancementRegistryItem

+export interface EnhancementRegistryItem

extends PersistableState

{ id: string; } diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index c85f48e01d48..6f545070040d 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { identity } from 'lodash'; import { @@ -23,7 +24,6 @@ import { } from '../common/lib'; import { PersistableStateService, - SerializableState, PersistableStateMigrateFn, MigrateFunctionsObject, } from '../../kibana_utils/common'; @@ -96,7 +96,7 @@ export class EmbeddableServerPlugin implements Plugin { + ((state: SerializableRecord) => { return { state, references: [] }; }), migrations: enhancement.migrations || {}, @@ -109,7 +109,7 @@ export class EmbeddableServerPlugin implements Plugin stats, inject: identity, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return { state, references: [] }; }, migrations: {}, diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index f8f3dcb0aa0b..e17f40423b00 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -6,7 +6,9 @@ import { CoreSetup } from 'kibana/server'; import { CoreStart } from 'kibana/server'; +import { KibanaExecutionContext } from 'src/core/public'; import { Plugin } from 'kibana/server'; +import { SerializableRecord } from '@kbn/utility-types'; // Warning: (ae-forgotten-export) The symbol "EmbeddableStateWithType" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts @@ -38,11 +40,10 @@ export interface EmbeddableSetup extends PersistableStateService; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ +export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ // (undocumented) id: string; } diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts index a07d036e1bdd..ba5314692e43 100644 --- a/src/plugins/embeddable/server/types.ts +++ b/src/plugins/embeddable/server/types.ts @@ -6,22 +6,19 @@ * Side Public License, v 1. */ -import { - PersistableState, - PersistableStateDefinition, - SerializableState, -} from '../../kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { PersistableState, PersistableStateDefinition } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; export type EmbeddableFactoryRegistry = Map; export type EnhancementsRegistry = Map; -export interface EnhancementRegistryDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ id: string; } -export interface EnhancementRegistryItem

+export interface EnhancementRegistryItem

extends PersistableState

{ id: string; } diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index 27a887500fb6..b7a6ff3252d6 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index 5f136d09b2ce..38e6cf78f8f6 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_error/tsconfig.json b/src/plugins/expression_error/tsconfig.json index aa4562ec7357..111ff58935a3 100644 --- a/src/plugins/expression_error/tsconfig.json +++ b/src/plugins/expression_error/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_image/tsconfig.json b/src/plugins/expression_image/tsconfig.json index 5fab51496c97..9a7175a8d767 100644 --- a/src/plugins/expression_image/tsconfig.json +++ b/src/plugins/expression_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_metric/tsconfig.json b/src/plugins/expression_metric/tsconfig.json index 5fab51496c97..9a7175a8d767 100644 --- a/src/plugins/expression_metric/tsconfig.json +++ b/src/plugins/expression_metric/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_repeat_image/tsconfig.json b/src/plugins/expression_repeat_image/tsconfig.json index aa4562ec7357..111ff58935a3 100644 --- a/src/plugins/expression_repeat_image/tsconfig.json +++ b/src/plugins/expression_repeat_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_reveal_image/tsconfig.json b/src/plugins/expression_reveal_image/tsconfig.json index aa4562ec7357..111ff58935a3 100644 --- a/src/plugins/expression_reveal_image/tsconfig.json +++ b/src/plugins/expression_reveal_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_shape/tsconfig.json b/src/plugins/expression_shape/tsconfig.json index 5fab51496c97..9a7175a8d767 100644 --- a/src/plugins/expression_shape/tsconfig.json +++ b/src/plugins/expression_shape/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 19537b3f164e..06eac98feba6 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; -import { ExpressionType, SerializableState } from '../expression_types'; +import { ExpressionType } from '../expression_types'; import { Adapters, RequestAdapter } from '../../../inspector/common'; import { TablesAdapter } from '../util/tables_adapter'; @@ -20,7 +21,7 @@ import { TablesAdapter } from '../util/tables_adapter'; */ export interface ExecutionContext< InspectorAdapters extends Adapters = Adapters, - ExecutionContextSearch extends SerializableState = SerializableState + ExecutionContextSearch extends SerializableRecord = SerializableRecord > { /** * Get search context of the expression. @@ -67,7 +68,7 @@ export interface ExecutionContext< /** * Contains the meta-data about the source of the expression. */ - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; } /** diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 930c9a4f0424..2767c8bc6ecb 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -10,6 +10,7 @@ import { cloneDeep, mapValues } from 'lodash'; import { Observable } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; @@ -25,7 +26,6 @@ import { MigrateFunctionsObject, migrateToLatest, PersistableStateService, - SerializableState, VersionedState, } from '../../../kibana_utils/common'; import { ExpressionExecutionParams } from '../service'; @@ -272,7 +272,7 @@ export class Executor = Record { if (!fn.migrations[version]) return link; const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index a4cb11411049..963d2186af73 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -7,12 +7,13 @@ */ import { identity } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; import { AnyExpressionFunctionDefinition } from './types'; import { ExpressionFunctionParameter } from './expression_function_parameter'; import { ExpressionValue } from '../expression_types/types'; import { ExpressionAstFunction } from '../ast'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState, SerializableState } from '../../../kibana_utils/common'; +import { PersistableState } from '../../../kibana_utils/common'; export class ExpressionFunction implements PersistableState { /** @@ -65,7 +66,7 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; constructor(functionDefinition: AnyExpressionFunctionDefinition) { diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index a094ce39d6ca..c268557936ac 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { map, pick, zipObject } from 'lodash'; import { ExpressionTypeDefinition } from '../types'; @@ -13,13 +14,6 @@ import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; import { SerializedFieldFormat } from '../../types'; -type State = string | number | boolean | null | undefined | SerializableState; - -/** @internal **/ -export interface SerializableState { - [key: string]: State | State[]; -} - const name = 'datatable'; /** @@ -84,7 +78,7 @@ export interface DatatableColumnMeta { /** * any extra parameters for the source that produced this column */ - sourceParams?: SerializableState; + sourceParams?: SerializableRecord; } /** diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index f8a95628c944..75e49633866f 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { ExpressionValueRender } from './render'; import { getType } from '../get_type'; -import { SerializableState } from '../../../../kibana_utils/common'; import { ErrorLike } from '../../util'; const name = 'error'; @@ -18,7 +18,7 @@ export type ExpressionValueError = ExpressionValueBoxed< 'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; } >; diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index cd52c8c3239d..b4dda3de5c93 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -7,9 +7,10 @@ */ import { Observable } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; @@ -18,11 +19,7 @@ import { ExecutionContract, ExecutionResult } from '../execution'; import { AnyExpressionTypeDefinition, ExpressionValueError } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; -import { - PersistableStateService, - SerializableState, - VersionedState, -} from '../../../kibana_utils/common'; +import { PersistableStateService, VersionedState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; import { clog, @@ -60,7 +57,7 @@ export type ExpressionsServiceSetup = Pick< >; export interface ExpressionExecutionParams { - searchContext?: SerializableState; + searchContext?: SerializableRecord; variables?: Record; @@ -84,7 +81,7 @@ export interface ExpressionExecutionParams { inspectorAdapters?: Adapters; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } /** diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 529d6653cb7f..3126af02286c 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -9,7 +9,7 @@ import { CoreStart } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; -import { IExecutionContextContainer } from 'src/core/public'; +import { KibanaExecutionContext } from 'src/core/public'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { ObservableLike } from '@kbn/utility-types'; @@ -17,6 +17,7 @@ import { PackageInfo } from '@kbn/config'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import React from 'react'; +import { SerializableRecord } from '@kbn/utility-types'; import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; @@ -133,13 +134,12 @@ export class Execution = StateContainer, ExecutionPureTransitions>; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; @@ -356,7 +356,7 @@ export class ExpressionFunction implements PersistableState SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; name: string; // (undocumented) @@ -764,7 +764,7 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -908,7 +908,7 @@ export interface IExpressionLoaderParams { // (undocumented) disableCaching?: boolean; // (undocumented) - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; // (undocumented) hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; // (undocumented) @@ -923,7 +923,7 @@ export interface IExpressionLoaderParams { // (undocumented) renderMode?: RenderMode; // (undocumented) - searchContext?: SerializableState_2; + searchContext?: SerializableRecord; // (undocumented) searchSessionId?: string; // (undocumented) @@ -1193,7 +1193,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // Warnings were encountered during analysis: // // src/plugins/expressions/common/ast/types.ts:29:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_functions/expression_function.ts:68:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:20:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 675ed7eeed7c..5a2198bb4f2e 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -5,13 +5,14 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { IExecutionContextContainer } from 'src/core/public'; + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { KibanaExecutionContext } from 'src/core/public'; import { Adapters } from '../../../inspector/public'; import { IInterpreterRenderHandlers, ExpressionValue, ExpressionsService, - SerializableState, RenderMode, } from '../../common'; import { ExpressionRenderHandlerParams } from '../render'; @@ -33,7 +34,7 @@ export interface ExpressionInterpreter { } export interface IExpressionLoaderParams { - searchContext?: SerializableState; + searchContext?: SerializableRecord; context?: ExpressionValue; variables?: Record; // Enables debug tracking on each expression in the AST @@ -48,7 +49,7 @@ export interface IExpressionLoaderParams { renderMode?: RenderMode; syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; /** * The flag to toggle on emitting partial results. diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 236c54db7140..05b8cb1a033d 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -8,12 +8,13 @@ import { CoreSetup } from 'src/core/server'; import { CoreStart } from 'src/core/server'; import { Ensure } from '@kbn/utility-types'; import { EventEmitter } from 'events'; -import { IExecutionContextContainer } from 'src/core/public'; +import { KibanaExecutionContext } from 'src/core/public'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { ObservableLike } from '@kbn/utility-types'; import { Plugin as Plugin_2 } from 'src/core/server'; import { PluginInitializerContext } from 'src/core/server'; +import { SerializableRecord } from '@kbn/utility-types'; import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; @@ -131,13 +132,12 @@ export class Execution = StateContainer, ExecutionPureTransitions>; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; @@ -328,7 +328,7 @@ export class ExpressionFunction implements PersistableState SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; name: string; // (undocumented) @@ -604,7 +604,7 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -947,7 +947,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // Warnings were encountered during analysis: // // src/plugins/expressions/common/ast/types.ts:29:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_functions/expression_function.ts:68:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:20:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index cce71013cefa..6716149d6b9c 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/field_formats/tsconfig.json b/src/plugins/field_formats/tsconfig.json index 4382ab1051c1..9fb87bc5dd97 100644 --- a/src/plugins/field_formats/tsconfig.json +++ b/src/plugins/field_formats/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 971b426458ce..1f0ce6186bb8 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -13,25 +13,45 @@ import { SavedObject } from 'kibana/server'; export const getSavedObjects = (): SavedObject[] => [ { - id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', - type: 'visualization', - updated_at: '2021-07-16T20:14:25.894Z', - version: '3', + attributes: { + fieldAttrs: + '{"products.manufacturer":{"count":1},"products.price":{"count":1},"products.product_name":{"count":1},"total_quantity":{"count":1}}', + fieldFormatMap: + '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}},"products.price":{"id":"number","params":{"pattern":"$0,0.00"}},"taxless_total_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxless_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxful_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.min_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_unit_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_price":{"id":"number","params":{"pattern":"$0,0.00"}}}', + fields: '[]', + runtimeFieldMap: '{}', + timeFieldName: 'order_date', + title: 'kibana_sample_data_ecommerce', + typeMeta: '{}', + }, + coreMigrationVersion: '8.0.0', + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', migrationVersion: { - visualization: '7.14.0', + 'index-pattern': '7.11.0', }, + references: [], + type: 'index-pattern', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzI1LDFd', + }, + { attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, title: i18n.translate('home.sampleData.ecommerceSpec.promotionTrackingTitle', { defaultMessage: '[eCommerce] Promotion Tracking', }), - visState: - '{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', uiStateJSON: '{}', - description: '', version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', - }, + visState: + '{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', + }, + coreMigrationVersion: '8.0.0', + id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + visualization: '7.14.0', }, references: [ { @@ -45,27 +65,28 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'visualization', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIxLDFd', }, { - id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', - type: 'visualization', - updated_at: '2021-07-14T20:45:27.899Z', - version: '2', - migrationVersion: { - visualization: '7.14.0', - }, attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, title: i18n.translate('home.sampleData.ecommerceSpec.soldProductsPerDayTitle', { defaultMessage: '[eCommerce] Sold Products per Day', }), - visState: - '{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', uiStateJSON: '{}', - description: '', version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', - }, + visState: + '{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', + }, + coreMigrationVersion: '8.0.0', + id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + visualization: '7.14.0', }, references: [ { @@ -74,21 +95,12 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'visualization', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIyLDFd', }, { - id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', - type: 'search', - updated_at: '2021-07-16T20:05:53.880Z', - version: '2', - migrationVersion: { - search: '7.9.3', - }, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.ordersTitle', { - defaultMessage: '[eCommerce] Orders', - }), - description: '', - hits: 0, columns: [ 'category', 'taxful_total_price', @@ -97,12 +109,22 @@ export const getSavedObjects = (): SavedObject[] => [ 'products.manufacturer', 'sku', ], - sort: [['order_date', 'desc']], - version: 1, + description: '', + hits: 0, kibanaSavedObjectMeta: { searchSourceJSON: '{"highlightAll":true,"version":true,"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, + sort: [['order_date', 'desc']], + title: i18n.translate('home.sampleData.ecommerceSpec.ordersTitle', { + defaultMessage: '[eCommerce] Orders', + }), + version: 1, + }, + coreMigrationVersion: '8.0.0', + id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + search: '7.9.3', }, references: [ { @@ -111,225 +133,1286 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'search', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIzLDFd', }, { - id: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - type: 'visualization', - updated_at: '2018-10-01T15:13:03.270Z', - version: '1', - migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', { - defaultMessage: '[eCommerce] Sales Count Map', - }), - visState: - '{"title":"[eCommerce] Sales Count Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_ecommerce\\n %context%: true\\n %timefield%: order_date\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geoip.location\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geoip.location\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n }\\n }\\n },\\n {\\n name: gridLabel\\n type: text\\n from: {data: \\"table\\"}\\n encode: {\\n enter: {\\n fill: {value: \\"firebrick\\"}\\n text: {signal: \\"datum.doc_count\\"}\\n }\\n update: {\\n x: {signal: \\"datum.x\\"}\\n y: {signal: \\"datum.y\\"}\\n dx: {value: -6}\\n dy: {value: 6}\\n fontSize: {value: 18}\\n fontWeight: {value: \\"bold\\"}\\n }\\n }\\n }\\n ]\\n}"}}', - uiStateJSON: '{}', description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"version":true,"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', - }, + layerListJSON: + '[{"id":"0hmz5","alpha":1,"sourceDescriptor":{"type":"EMS_TMS","isAutoSelect":true},"visible":true,"style":{},"type":"VECTOR_TILE","minZoom":0,"maxZoom":24},{"id":"7ameq","label":null,"minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries","tooltipProperties":["name","iso2"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__741db9c6-8ebb-4ea9-9885-b6b4ac019d14","origin":"join"},"color":"Green to Red","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"type":"ES_TERM_SOURCE","id":"741db9c6-8ebb-4ea9-9885-b6b4ac019d14","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.country_iso_code","indexPatternRefName":"layer_1_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"jmtgf","label":"United States","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"usa_states","tooltipProperties":["name"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__30a0ec24-49b6-476a-b4ed-6c1636333695","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"name","right":{"type":"ES_TERM_SOURCE","id":"30a0ec24-49b6-476a-b4ed-6c1636333695","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_2_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"ui5f8","label":"France","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"france_departments","tooltipProperties":["label_en"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__e325c9da-73fa-4b3b-8b59-364b99370826","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"label_en","right":{"type":"ES_TERM_SOURCE","id":"e325c9da-73fa-4b3b-8b59-364b99370826","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_3_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"y3fjb","label":"United Kingdom","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"uk_subdivisions","tooltipProperties":["label_en"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__612d805d-8533-43a9-ac0e-cbf51fe63dcd","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"label_en","right":{"type":"ES_TERM_SOURCE","id":"612d805d-8533-43a9-ac0e-cbf51fe63dcd","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_4_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"c54wk","label":"Sales","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"04c983b0-8cfa-4e6a-a64b-52c10b7008fe","type":"ES_SEARCH","geoField":"geoip.location","limit":2048,"filterByMapBounds":true,"tooltipProperties":["category","customer_gender","manufacturer","order_id","total_quantity","total_unique_products","taxful_total_price","order_date","geoip.region_name","geoip.country_iso_code"],"indexPatternRefName":"layer_5_source_index_pattern","applyGlobalQuery":true,"scalingType":"LIMIT"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"taxful_total_price","origin":"source"},"color":"Greens","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR"},{"id":"qvhh3","label":"Total Sales Revenue","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"aa7f87b8-9dc5-42be-b19e-1a2fa09b6cad","geoField":"geoip.location","requestType":"point","metrics":[{"type":"count","label":"sales count"},{"type":"sum","field":"taxful_total_price","label":"total sales price"}],"indexPatternRefName":"layer_6_source_index_pattern","applyGlobalQuery":true},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"doc_count","origin":"source"},"color":"Greens","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"minSize":1,"maxSize":20,"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelText":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelSize":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"minSize":12,"maxSize":24,"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelBorderSize":{"options":{"size":"MEDIUM"}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR"}]', + mapStateJSON: + '{"zoom":2.11,"center":{"lon":-15.07605,"lat":45.88578},"timeFilters":{"from":"now-7d","to":"now"},"refreshConfig":{"isPaused":true,"interval":0},"query":{"query":"","language":"kuery"},"settings":{"autoFitToDataBounds":false}}', + title: '[eCommerce] Orders by Country', + uiStateJSON: '{"isDarkMode":false}', + }, + coreMigrationVersion: '8.0.0', + id: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', + migrationVersion: { + map: '7.14.0', }, references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + name: 'layer_1_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_2_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_3_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_4_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_5_source_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_6_source_index_pattern', type: 'index-pattern', }, ], + type: 'map', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzI5LDFd', }, { - id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - type: 'index-pattern', - updated_at: '2021-07-16T20:08:12.675Z', - version: '2', - migrationVersion: { - 'index-pattern': '7.11.0', - }, attributes: { - title: 'kibana_sample_data_ecommerce', - timeFieldName: 'order_date', - fieldAttrs: - '{"products.manufacturer":{"count":1},"products.price":{"count":1},"products.product_name":{"count":1},"total_quantity":{"count":1}}', - fieldFormatMap: - '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}},"products.price":{"id":"number","params":{"pattern":"$0,0.00"}},"taxless_total_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxless_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxful_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.min_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_unit_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_price":{"id":"number","params":{"pattern":"$0,0.00"}}}', - fields: '[]', - runtimeFieldMap: '{}', - typeMeta: '{}', + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, + title: '[eCommerce] Markdown', + uiStateJSON: '{}', + version: 1, + visState: + '{"title":"[eCommerce] Markdown","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"### Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"aggs":[]}', + }, + coreMigrationVersion: '8.0.0', + id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + visualization: '7.14.0', }, references: [], + type: 'visualization', + updated_at: '2021-08-05T12:43:35.817Z', + version: 'WzE3MSwxXQ==', }, { - id: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', - type: 'dashboard', - updated_at: '2021-07-16T20:43:03.136Z', - version: '2', - references: [ - { - id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', - name: '5:panel_5', - type: 'visualization', - }, - { - id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', - name: '7:panel_7', - type: 'visualization', - }, - { - id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', - name: '10:panel_10', - type: 'search', - }, - { - id: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - name: '11:panel_11', - type: 'map', + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', }, + title: '[eCommerce] Controls', + uiStateJSON: '{}', + version: 1, + visState: + '{"title":"[eCommerce] Controls","type":"input_control_vis","params":{"controls":[{"id":"1536977437774","fieldName":"manufacturer.keyword","parent":"","label":"Manufacturer","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_0_index_pattern"},{"id":"1536977465554","fieldName":"category.keyword","parent":"","label":"Category","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_1_index_pattern"},{"id":"1536977596163","fieldName":"total_quantity","parent":"","label":"Quantity","type":"range","options":{"decimalPlaces":0,"step":1},"indexPatternRefName":"control_2_index_pattern"}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"aggs":[]}', + }, + coreMigrationVersion: '8.0.0', + id: 'c3378480-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + visualization: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_0_index_pattern', + name: 'control_0_index_pattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_1_index_pattern', + name: 'control_1_index_pattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_2_index_pattern', + name: 'control_2_index_pattern', type: 'index-pattern', }, + ], + type: 'visualization', + updated_at: '2021-08-05T12:43:41.128Z', + version: 'WzE3NiwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'c7478794-6767-4286-9d65-1c0ecd909dd8': { + columnOrder: [ + '8289349e-6d1b-4abf-b164-0208183d2c34', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X0', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X1', + ], + columns: { + '041db33b-5c9c-47f3-a5d3-ef5e255d1663': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: '% of target ($10k)', + operationType: 'formula', + params: { + format: { + id: 'percent', + params: { + decimals: 0, + }, + }, + formula: 'sum(taxful_total_price) / 10000 - 1', + isFormulaBroken: false, + }, + references: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X1'], + scale: 'ratio', + }, + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Weekly revenue', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X1': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Weekly revenue', + operationType: 'math', + params: { + tinymathAst: { + args: [ + { + args: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X0', 10000], + location: { + max: 32, + min: 0, + }, + name: 'divide', + text: 'sum(taxful_total_price) / 10000 ', + type: 'function', + }, + 1, + ], + location: { + max: 35, + min: 0, + }, + name: 'subtract', + text: 'sum(taxful_total_price) / 10000 - 1', + type: 'function', + }, + }, + references: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X0'], + scale: 'ratio', + }, + '8289349e-6d1b-4abf-b164-0208183d2c34': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663'], + layerId: 'c7478794-6767-4286-9d65-1c0ecd909dd8', + seriesType: 'bar_stacked', + xAccessor: '8289349e-6d1b-4abf-b164-0208183d2c34', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_stacked', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: '% of target revenue ($10k)', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'c65434d6-fe64-460f-b07a-c7d267c856ff:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'c65434d6-fe64-460f-b07a-c7d267c856ff:indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', + name: 'indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:43:48.122Z', + version: 'WzE4NCwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'c7478794-6767-4286-9d65-1c0ecd909dd8': { + columnOrder: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663'], + columns: { + '041db33b-5c9c-47f3-a5d3-ef5e255d1663': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Sum of revenue', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: '041db33b-5c9c-47f3-a5d3-ef5e255d1663', + layerId: 'c7478794-6767-4286-9d65-1c0ecd909dd8', + }, + }, + title: 'Sum of revenue', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '2e6ef14d-7b03-46d4-a6b8-a962ee36a805:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '2e6ef14d-7b03-46d4-a6b8-a962ee36a805:indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', + name: 'indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:43:59.238Z', + version: 'WzE4OSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '4fb42a8e-b133-43c8-805c-a38472053938': { + columnOrder: ['020bbfdf-9ef8-4802-aa9e-342d2ea0bebf'], + columns: { + '020bbfdf-9ef8-4802-aa9e-342d2ea0bebf': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Median spending', + operationType: 'median', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: '020bbfdf-9ef8-4802-aa9e-342d2ea0bebf', + layerId: '4fb42a8e-b133-43c8-805c-a38472053938', + }, + }, + title: 'Median spending', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '5108a3bc-d1cf-4255-8c95-2df52577b956:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '5108a3bc-d1cf-4255-8c95-2df52577b956:indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938', + name: 'indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:12.595Z', + version: 'WzE5NywxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'b6093a53-884f-42c2-9fcc-ba56cfb66c53': { + columnOrder: [ + '15c45f89-a149-443a-a830-aa8c3a9317db', + '2b41b3d8-2f62-407a-a866-960f254c679d', + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + 'ddc92e50-4d5c-413e-b91b-3e504889fa65', + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + ], + columns: { + '15c45f89-a149-443a-a830-aa8c3a9317db': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + '2b41b3d8-2f62-407a-a866-960f254c679d': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Total items', + operationType: 'sum', + scale: 'ratio', + sourceField: 'products.quantity', + }, + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Tx. last week', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + timeShift: '1w', + }, + 'ddc92e50-4d5c-413e-b91b-3e504889fa65': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Transactions', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Last week', + operationType: 'sum', + scale: 'ratio', + sourceField: 'products.quantity', + timeShift: '1w', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + curveType: 'LINEAR', + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: [ + '2b41b3d8-2f62-407a-a866-960f254c679d', + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + 'ddc92e50-4d5c-413e-b91b-3e504889fa65', + ], + layerId: 'b6093a53-884f-42c2-9fcc-ba56cfb66c53', + position: 'top', + seriesType: 'line', + showGridlines: false, + xAccessor: '15c45f89-a149-443a-a830-aa8c3a9317db', + yConfig: [ + { + color: '#b6e0d5', + forAccessor: 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + }, + { + color: '#edafc4', + forAccessor: '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + }, + ], + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Transactions per day', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '6bc3fa4a-8f1b-436f-afc1-f3516ee531ce:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '6bc3fa4a-8f1b-436f-afc1-f3516ee531ce:indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53', + name: 'indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:25.915Z', + version: 'WzIwMywxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17': { + columnOrder: ['c52c2003-ae58-4604-bae7-52ba0fb38a01'], + columns: { + 'c52c2003-ae58-4604-bae7-52ba0fb38a01': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Avg. items sold', + operationType: 'average', + params: { + format: { + id: 'number', + params: { + decimals: 1, + }, + }, + }, + scale: 'ratio', + sourceField: 'total_quantity', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: 'c52c2003-ae58-4604-bae7-52ba0fb38a01', + layerId: '667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', + }, + }, + title: 'Avg. items sold', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '222c1f05-ca21-4e62-a04a-9a059b4534a7:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '222c1f05-ca21-4e62-a04a-9a059b4534a7:indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', + name: 'indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:35.396Z', + version: 'WzIwOSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '97c63ea6-9305-4755-97d1-0f26817c6f9a': { + columnOrder: [ + '9f61a7df-198e-4754-b34c-81ed544136ba', + 'ebcb19af-0900-4439-949f-d8cd9bccde19', + '5575214b-7f21-4b6c-8bc1-34433c6a0c58', + ], + columns: { + '5575214b-7f21-4b6c-8bc1-34433c6a0c58': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + '9f61a7df-198e-4754-b34c-81ed544136ba': { + dataType: 'string', + isBucketed: true, + label: 'Top values of category.keyword', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '5575214b-7f21-4b6c-8bc1-34433c6a0c58', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: true, + size: 10, + }, + scale: 'ordinal', + sourceField: 'category.keyword', + }, + 'ebcb19af-0900-4439-949f-d8cd9bccde19': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['5575214b-7f21-4b6c-8bc1-34433c6a0c58'], + layerId: '97c63ea6-9305-4755-97d1-0f26817c6f9a', + position: 'top', + seriesType: 'bar_percentage_stacked', + showGridlines: false, + splitAccessor: '9f61a7df-198e-4754-b34c-81ed544136ba', + xAccessor: 'ebcb19af-0900-4439-949f-d8cd9bccde19', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_percentage_stacked', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Breakdown by category', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'a885226c-6830-4731-88a0-8c1d1047841e:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a885226c-6830-4731-88a0-8c1d1047841e:indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7', + name: 'indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:52.693Z', + version: 'WzIxNSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '0731ee8b-31c5-4be9-92d9-69ee760465d7': { + columnOrder: [ + '7bf8f089-1542-40bd-b349-45fdfc309ac6', + '826b2f39-b616-40b2-a222-972fdc1d7596', + 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X2', + ], + columns: { + '7bf8f089-1542-40bd-b349-45fdfc309ac6': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + '826b2f39-b616-40b2-a222-972fdc1d7596': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'This week', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Difference', + operationType: 'formula', + params: { + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + formula: "sum(taxful_total_price) - sum(taxful_total_price, shift='1w')", + isFormulaBroken: false, + }, + references: ['bf51c1af-443e-49f4-a21f-54c87bfc5677X2'], + scale: 'ratio', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + timeShift: '1w', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X2': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'math', + params: { + tinymathAst: { + args: [ + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + ], + location: { + max: 61, + min: 0, + }, + name: 'subtract', + text: "sum(taxful_total_price) - sum(taxful_total_price, shift='1w')", + type: 'function', + }, + }, + references: [ + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + ], + scale: 'ratio', + }, + 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: '1 week ago', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + timeShift: '1w', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + columns: [ + { + columnId: '7bf8f089-1542-40bd-b349-45fdfc309ac6', + }, + { + alignment: 'left', + columnId: '826b2f39-b616-40b2-a222-972fdc1d7596', + }, + { + columnId: 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0', + }, + { + colorMode: 'text', + columnId: 'bf51c1af-443e-49f4-a21f-54c87bfc5677', + isTransposed: false, + palette: { + name: 'custom', + params: { + colorStops: [ + { + color: '#D36086', + stop: -10000, + }, + { + color: '#209280', + stop: 0, + }, + ], + continuity: 'above', + name: 'custom', + rangeMax: 0, + rangeMin: -10000, + rangeType: 'number', + steps: 5, + stops: [ + { + color: '#D36086', + stop: 0, + }, + { + color: '#209280', + stop: 2249.03125, + }, + ], + }, + type: 'palette', + }, + }, + ], + layerId: '0731ee8b-31c5-4be9-92d9-69ee760465d7', + }, + }, + title: 'Daily comparison', + visualizationType: 'lnsDatatable', + }, + coreMigrationVersion: '8.0.0', + id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '003bdfc7-4d9e-4bd0-b088-3b18f79588d1:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '003bdfc7-4d9e-4bd0-b088-3b18f79588d1:indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a', + name: 'indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:45:22.123Z', + version: 'WzIyMiwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '5ed846c2-a70b-4d9c-a244-f254bef763b8': { + columnOrder: [ + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + '7ac31901-277a-46e2-8128-8d684b2c1127', + ], + columns: { + '7ac31901-277a-46e2-8128-8d684b2c1127': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Items', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46': { + customLabel: true, + dataType: 'string', + isBucketed: true, + label: 'Product name', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '7ac31901-277a-46e2-8128-8d684b2c1127', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: false, + size: 5, + }, + scale: 'ordinal', + sourceField: 'products.product_name.keyword', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: true, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['7ac31901-277a-46e2-8128-8d684b2c1127'], + layerId: '5ed846c2-a70b-4d9c-a244-f254bef763b8', + position: 'top', + seriesType: 'bar_horizontal', + showGridlines: false, + xAccessor: 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_horizontal', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Top products this week', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: '03071e90-f5eb-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'b1697063-c817-4847-aa0d-5bed47137b7e:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'b1697063-c817-4847-aa0d-5bed47137b7e:indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', + name: 'indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:45:28.185Z', + version: 'WzIyOCwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '5ed846c2-a70b-4d9c-a244-f254bef763b8': { + columnOrder: [ + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + '7ac31901-277a-46e2-8128-8d684b2c1127', + ], + columns: { + '7ac31901-277a-46e2-8128-8d684b2c1127': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Items', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46': { + customLabel: true, + dataType: 'string', + isBucketed: true, + label: 'Product name', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '7ac31901-277a-46e2-8128-8d684b2c1127', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: false, + size: 5, + }, + scale: 'ordinal', + sourceField: 'products.product_name.keyword', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: true, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['7ac31901-277a-46e2-8128-8d684b2c1127'], + layerId: '5ed846c2-a70b-4d9c-a244-f254bef763b8', + position: 'top', + seriesType: 'bar_horizontal', + showGridlines: false, + xAccessor: 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_horizontal', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Top products last week', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: '06379e00-f5eb-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba:indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', + name: 'indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', type: 'index-pattern', }, ], - migrationVersion: { - dashboard: '7.14.0', - }, + type: 'lens', + updated_at: '2021-08-05T12:45:33.536Z', + version: 'WzIzMywxXQ==', + }, + { attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardTitle', { - defaultMessage: '[eCommerce] Revenue Dashboard', - }), - hits: 0, description: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardDescription', { defaultMessage: 'Analyze mock eCommerce orders and revenue', }), + hits: 0, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}', + }, optionsJSON: '{"hidePanelTitles":false,"useMargins":true}', panelsJSON: - '[{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":22,"w":24,"h":10,"i":"5"},"panelIndex":"5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_5"},{"version":"7.14.0","type":"visualization","gridData":{"x":36,"y":15,"w":12,"h":7,"i":"7"},"panelIndex":"7","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_7"},{"version":"7.14.0","type":"search","gridData":{"x":0,"y":55,"w":48,"h":18,"i":"10"},"panelIndex":"10","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_10"},{"version":"7.14.0","type":"map","gridData":{"x":0,"y":32,"w":24,"h":14,"i":"11"},"panelIndex":"11","embeddableConfig":{"isLayerTOCOpen":false,"enhancements":{},"mapCenter":{"lat":45.88578,"lon":-15.07605,"zoom":2.11},"mapBuffer":{"minLon":-90,"minLat":0,"maxLon":45,"maxLat":66.51326},"openTOCDetails":[],"hiddenLayers":[]},"panelRefName":"panel_11"},{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":0,"w":18,"h":7,"i":"585b11d3-3461-49a7-8f5b-f56521b9dc8b"},"panelIndex":"585b11d3-3461-49a7-8f5b-f56521b9dc8b","embeddableConfig":{"savedVis":{"title":"[eCommerce] Markdown","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"### Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"enhancements":{}}},{"version":"7.14.0","type":"visualization","gridData":{"x":18,"y":0,"w":30,"h":7,"i":"a5914d17-81fe-4f27-b240-23ac529c1499"},"panelIndex":"a5914d17-81fe-4f27-b240-23ac529c1499","embeddableConfig":{"savedVis":{"title":"[eCommerce] Controls","description":"","type":"input_control_vis","params":{"controls":[{"id":"1536977437774","fieldName":"manufacturer.keyword","parent":"","label":"Manufacturer","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_0_index_pattern"},{"id":"1536977465554","fieldName":"category.keyword","parent":"","label":"Category","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_1_index_pattern"},{"id":"1536977596163","fieldName":"total_quantity","parent":"","label":"Quantity","type":"range","options":{"decimalPlaces":0,"step":1},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_2_index_pattern"}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":7,"w":24,"h":8,"i":"c65434d6-fe64-460f-b07a-c7d267c856ff"},"panelIndex":"c65434d6-fe64-460f-b07a-c7d267c856ff","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"c7478794-6767-4286-9d65-1c0ecd909dd8":{"columns":{"8289349e-6d1b-4abf-b164-0208183d2c34":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"041db33b-5c9c-47f3-a5d3-ef5e255d1663X0":{"label":"Part of Weekly revenue","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"041db33b-5c9c-47f3-a5d3-ef5e255d1663X1":{"label":"Part of Weekly revenue","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":[{"type":"function","name":"divide","args":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X0",10000],"location":{"min":0,"max":32},"text":"sum(taxful_total_price) / 10000 "},1],"location":{"min":0,"max":35},"text":"sum(taxful_total_price) / 10000 - 1"}},"references":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X0"],"customLabel":true},"041db33b-5c9c-47f3-a5d3-ef5e255d1663":{"label":"% of target ($10k)","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"sum(taxful_total_price) / 10000 - 1","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":0}}},"references":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X1"],"customLabel":true}},"columnOrder":["8289349e-6d1b-4abf-b164-0208183d2c34","041db33b-5c9c-47f3-a5d3-ef5e255d1663","041db33b-5c9c-47f3-a5d3-ef5e255d1663X0","041db33b-5c9c-47f3-a5d3-ef5e255d1663X1"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_stacked","layers":[{"layerId":"c7478794-6767-4286-9d65-1c0ecd909dd8","seriesType":"bar_stacked","xAccessor":"8289349e-6d1b-4abf-b164-0208183d2c34","accessors":["041db33b-5c9c-47f3-a5d3-ef5e255d1663"]}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8"}]},"enhancements":{},"hidePanelTitles":false},"title":"% of target revenue ($10k)"},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":7,"w":12,"h":8,"i":"2e6ef14d-7b03-46d4-a6b8-a962ee36a805"},"panelIndex":"2e6ef14d-7b03-46d4-a6b8-a962ee36a805","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"c7478794-6767-4286-9d65-1c0ecd909dd8":{"columns":{"041db33b-5c9c-47f3-a5d3-ef5e255d1663":{"label":"Sum of revenue","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true}},"columnOrder":["041db33b-5c9c-47f3-a5d3-ef5e255d1663"],"incompleteColumns":{}}}}},"visualization":{"layerId":"c7478794-6767-4286-9d65-1c0ecd909dd8","accessor":"041db33b-5c9c-47f3-a5d3-ef5e255d1663"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":36,"y":7,"w":12,"h":8,"i":"5108a3bc-d1cf-4255-8c95-2df52577b956"},"panelIndex":"5108a3bc-d1cf-4255-8c95-2df52577b956","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"4fb42a8e-b133-43c8-805c-a38472053938":{"columns":{"020bbfdf-9ef8-4802-aa9e-342d2ea0bebf":{"label":"Median spending","dataType":"number","operationType":"median","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true}},"columnOrder":["020bbfdf-9ef8-4802-aa9e-342d2ea0bebf"],"incompleteColumns":{}}}}},"visualization":{"layerId":"4fb42a8e-b133-43c8-805c-a38472053938","accessor":"020bbfdf-9ef8-4802-aa9e-342d2ea0bebf"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":15,"w":24,"h":7,"i":"6bc3fa4a-8f1b-436f-afc1-f3516ee531ce"},"panelIndex":"6bc3fa4a-8f1b-436f-afc1-f3516ee531ce","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"b6093a53-884f-42c2-9fcc-ba56cfb66c53":{"columns":{"15c45f89-a149-443a-a830-aa8c3a9317db":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"2b41b3d8-2f62-407a-a866-960f254c679d":{"label":"Total items","dataType":"number","operationType":"sum","sourceField":"products.quantity","isBucketed":false,"scale":"ratio","customLabel":true},"ddc92e50-4d5c-413e-b91b-3e504889fa65":{"label":"Transactions","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"eadae280-2da3-4d1d-a0e1-f9733f89c15b":{"label":"Last week","dataType":"number","operationType":"sum","sourceField":"products.quantity","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"5e31e5d3-2aaa-4475-a130-3b69bf2f748a":{"label":"Tx. last week","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","timeShift":"1w","customLabel":true}},"columnOrder":["15c45f89-a149-443a-a830-aa8c3a9317db","2b41b3d8-2f62-407a-a866-960f254c679d","eadae280-2da3-4d1d-a0e1-f9733f89c15b","ddc92e50-4d5c-413e-b91b-3e504889fa65","5e31e5d3-2aaa-4475-a130-3b69bf2f748a"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"line","layers":[{"layerId":"b6093a53-884f-42c2-9fcc-ba56cfb66c53","accessors":["2b41b3d8-2f62-407a-a866-960f254c679d","eadae280-2da3-4d1d-a0e1-f9733f89c15b","5e31e5d3-2aaa-4475-a130-3b69bf2f748a","ddc92e50-4d5c-413e-b91b-3e504889fa65"],"position":"top","seriesType":"line","showGridlines":false,"xAccessor":"15c45f89-a149-443a-a830-aa8c3a9317db","yConfig":[{"forAccessor":"eadae280-2da3-4d1d-a0e1-f9733f89c15b","color":"#b6e0d5"},{"forAccessor":"5e31e5d3-2aaa-4475-a130-3b69bf2f748a","color":"#edafc4"}]}],"curveType":"LINEAR"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":15,"w":12,"h":7,"i":"222c1f05-ca21-4e62-a04a-9a059b4534a7"},"panelIndex":"222c1f05-ca21-4e62-a04a-9a059b4534a7","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17":{"columns":{"c52c2003-ae58-4604-bae7-52ba0fb38a01":{"label":"Avg. items sold","dataType":"number","operationType":"average","sourceField":"total_quantity","isBucketed":false,"scale":"ratio","params":{"format":{"id":"number","params":{"decimals":1}}},"customLabel":true}},"columnOrder":["c52c2003-ae58-4604-bae7-52ba0fb38a01"],"incompleteColumns":{}}}}},"visualization":{"layerId":"667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17","accessor":"c52c2003-ae58-4604-bae7-52ba0fb38a01"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":22,"w":24,"h":10,"i":"003bdfc7-4d9e-4bd0-b088-3b18f79588d1"},"panelIndex":"003bdfc7-4d9e-4bd0-b088-3b18f79588d1","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"97c63ea6-9305-4755-97d1-0f26817c6f9a":{"columns":{"9f61a7df-198e-4754-b34c-81ed544136ba":{"label":"Top values of category.keyword","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"category.keyword","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"5575214b-7f21-4b6c-8bc1-34433c6a0c58"},"orderDirection":"desc","otherBucket":true,"missingBucket":false}},"ebcb19af-0900-4439-949f-d8cd9bccde19":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"5575214b-7f21-4b6c-8bc1-34433c6a0c58":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records"}},"columnOrder":["9f61a7df-198e-4754-b34c-81ed544136ba","ebcb19af-0900-4439-949f-d8cd9bccde19","5575214b-7f21-4b6c-8bc1-34433c6a0c58"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_percentage_stacked","layers":[{"layerId":"97c63ea6-9305-4755-97d1-0f26817c6f9a","accessors":["5575214b-7f21-4b6c-8bc1-34433c6a0c58"],"position":"top","seriesType":"bar_percentage_stacked","showGridlines":false,"xAccessor":"ebcb19af-0900-4439-949f-d8cd9bccde19","splitAccessor":"9f61a7df-198e-4754-b34c-81ed544136ba"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":32,"w":24,"h":14,"i":"a885226c-6830-4731-88a0-8c1d1047841e"},"panelIndex":"a885226c-6830-4731-88a0-8c1d1047841e","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsDatatable","state":{"datasourceStates":{"indexpattern":{"layers":{"0731ee8b-31c5-4be9-92d9-69ee760465d7":{"columns":{"7bf8f089-1542-40bd-b349-45fdfc309ac6":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"826b2f39-b616-40b2-a222-972fdc1d7596":{"label":"This week","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"cfd45c47-fc41-430c-9e7a-b71dc0c916b0":{"label":"1 week ago","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X0":{"label":"Part of Difference","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X1":{"label":"Part of Difference","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X2":{"label":"Part of Difference","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":["bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1"],"location":{"min":0,"max":61},"text":"sum(taxful_total_price) - sum(taxful_total_price, shift=\'1w\')"}},"references":["bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1"],"customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677":{"label":"Difference","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"sum(taxful_total_price) - sum(taxful_total_price, shift=\'1w\')","isFormulaBroken":false,"format":{"id":"number","params":{"decimals":2}}},"references":["bf51c1af-443e-49f4-a21f-54c87bfc5677X2"],"customLabel":true}},"columnOrder":["7bf8f089-1542-40bd-b349-45fdfc309ac6","826b2f39-b616-40b2-a222-972fdc1d7596","cfd45c47-fc41-430c-9e7a-b71dc0c916b0","bf51c1af-443e-49f4-a21f-54c87bfc5677","bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1","bf51c1af-443e-49f4-a21f-54c87bfc5677X2"],"incompleteColumns":{}}}}},"visualization":{"layerId":"0731ee8b-31c5-4be9-92d9-69ee760465d7","columns":[{"columnId":"7bf8f089-1542-40bd-b349-45fdfc309ac6"},{"columnId":"826b2f39-b616-40b2-a222-972fdc1d7596","alignment":"left"},{"columnId":"cfd45c47-fc41-430c-9e7a-b71dc0c916b0"},{"columnId":"bf51c1af-443e-49f4-a21f-54c87bfc5677","isTransposed":false,"colorMode":"text","palette":{"name":"custom","type":"palette","params":{"steps":5,"stops":[{"color":"#D36086","stop":0},{"color":"#209280","stop":2249.03125}],"continuity":"above","rangeType":"number","colorStops":[{"color":"#D36086","stop":-10000},{"color":"#209280","stop":0}],"rangeMin":-10000,"rangeMax":0,"name":"custom"}}}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":46,"w":24,"h":9,"i":"562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba"},"panelIndex":"562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"5ed846c2-a70b-4d9c-a244-f254bef763b8":{"columns":{"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46":{"label":"Product name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"products.product_name.keyword","isBucketed":true,"params":{"size":5,"orderBy":{"type":"column","columnId":"7ac31901-277a-46e2-8128-8d684b2c1127"},"orderDirection":"desc","otherBucket":false,"missingBucket":false},"customLabel":true},"7ac31901-277a-46e2-8128-8d684b2c1127":{"label":"Items","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true}},"columnOrder":["d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46","7ac31901-277a-46e2-8128-8d684b2c1127"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":true,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_horizontal","layers":[{"layerId":"5ed846c2-a70b-4d9c-a244-f254bef763b8","accessors":["7ac31901-277a-46e2-8128-8d684b2c1127"],"position":"top","seriesType":"bar_horizontal","showGridlines":false,"xAccessor":"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8"}]},"timeRange":{"from":"now-2w","to":"now-1w"},"hidePanelTitles":false,"enhancements":{}},"title":"Top products last week"},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":46,"w":24,"h":9,"i":"b1697063-c817-4847-aa0d-5bed47137b7e"},"panelIndex":"b1697063-c817-4847-aa0d-5bed47137b7e","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"5ed846c2-a70b-4d9c-a244-f254bef763b8":{"columns":{"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46":{"label":"Product name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"products.product_name.keyword","isBucketed":true,"params":{"size":5,"orderBy":{"type":"column","columnId":"7ac31901-277a-46e2-8128-8d684b2c1127"},"orderDirection":"desc","otherBucket":false,"missingBucket":false},"customLabel":true},"7ac31901-277a-46e2-8128-8d684b2c1127":{"label":"Items","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true}},"columnOrder":["d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46","7ac31901-277a-46e2-8128-8d684b2c1127"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":true,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_horizontal","layers":[{"layerId":"5ed846c2-a70b-4d9c-a244-f254bef763b8","accessors":["7ac31901-277a-46e2-8128-8d684b2c1127"],"position":"top","seriesType":"bar_horizontal","showGridlines":false,"xAccessor":"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8"}]},"hidePanelTitles":false,"enhancements":{}},"title":"Top products this week"}]', - version: 1, - timeRestore: true, - timeTo: 'now', - timeFrom: 'now-7d', + '[{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":0,"y":22,"w":24,"h":10,"i":"5"},"panelIndex":"5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_5"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":36,"y":15,"w":12,"h":7,"i":"7"},"panelIndex":"7","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_7"},{"version":"8.0.0-SNAPSHOT","type":"search","gridData":{"x":0,"y":55,"w":48,"h":18,"i":"10"},"panelIndex":"10","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_10"},{"version":"8.0.0-SNAPSHOT","type":"map","gridData":{"x":0,"y":32,"w":24,"h":14,"i":"11"},"panelIndex":"11","embeddableConfig":{"isLayerTOCOpen":false,"enhancements":{},"mapCenter":{"lat":45.88578,"lon":-15.07605,"zoom":2.11},"mapBuffer":{"minLon":-135,"minLat":0,"maxLon":90,"maxLat":66.51326},"openTOCDetails":[],"hiddenLayers":[]},"panelRefName":"panel_11"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":0,"y":0,"w":18,"h":7,"i":"a71cf076-6895-491c-8878-63592e429ed5"},"panelIndex":"a71cf076-6895-491c-8878-63592e429ed5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_a71cf076-6895-491c-8878-63592e429ed5"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":18,"y":0,"w":30,"h":7,"i":"adc0a2f4-481c-45eb-b422-0ea59a3e5163"},"panelIndex":"adc0a2f4-481c-45eb-b422-0ea59a3e5163","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_adc0a2f4-481c-45eb-b422-0ea59a3e5163"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":7,"w":24,"h":8,"i":"7077b79f-2a99-4fcb-bbd4-456982843278"},"panelIndex":"7077b79f-2a99-4fcb-bbd4-456982843278","embeddableConfig":{"enhancements":{},"hidePanelTitles":false},"title":"% of target revenue ($10k)","panelRefName":"panel_7077b79f-2a99-4fcb-bbd4-456982843278"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":7,"w":12,"h":8,"i":"19a3c101-ad2e-4421-a71b-a4734ec1f03e"},"panelIndex":"19a3c101-ad2e-4421-a71b-a4734ec1f03e","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_19a3c101-ad2e-4421-a71b-a4734ec1f03e"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":36,"y":7,"w":12,"h":8,"i":"491469e7-7d24-4216-aeb3-bca00e5c8c1b"},"panelIndex":"491469e7-7d24-4216-aeb3-bca00e5c8c1b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_491469e7-7d24-4216-aeb3-bca00e5c8c1b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":15,"w":24,"h":7,"i":"a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef"},"panelIndex":"a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":15,"w":12,"h":7,"i":"da51079b-952f-43dc-96e6-6f9415a3708b"},"panelIndex":"da51079b-952f-43dc-96e6-6f9415a3708b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_da51079b-952f-43dc-96e6-6f9415a3708b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":22,"w":24,"h":10,"i":"64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b"},"panelIndex":"64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":32,"w":24,"h":14,"i":"bd330ede-2eef-4e2a-8100-22a21abf5038"},"panelIndex":"bd330ede-2eef-4e2a-8100-22a21abf5038","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_bd330ede-2eef-4e2a-8100-22a21abf5038"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":46,"w":24,"h":9,"i":"b897d4be-cf83-46fb-a111-c7fbec9ef403"},"panelIndex":"b897d4be-cf83-46fb-a111-c7fbec9ef403","embeddableConfig":{"hidePanelTitles":false,"enhancements":{}},"title":"Top products this week","panelRefName":"panel_b897d4be-cf83-46fb-a111-c7fbec9ef403"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":46,"w":24,"h":9,"i":"e0f68f93-30f2-4da7-889a-6cd128a68d3f"},"panelIndex":"e0f68f93-30f2-4da7-889a-6cd128a68d3f","embeddableConfig":{"timeRange":{"from":"now-2w","to":"now-1w"},"hidePanelTitles":false,"enhancements":{}},"title":"Top products last week","panelRefName":"panel_e0f68f93-30f2-4da7-889a-6cd128a68d3f"}]', refreshInterval: { pause: true, value: 0, }, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}', - }, + timeFrom: 'now-7d', + timeRestore: true, + timeTo: 'now', + title: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardTitle', { + defaultMessage: '[eCommerce] Revenue Dashboard', + }), + version: 1, + }, + coreMigrationVersion: '8.0.0', + id: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + dashboard: '7.14.0', }, + references: [ + { + id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', + name: '5:panel_5', + type: 'visualization', + }, + { + id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', + name: '7:panel_7', + type: 'visualization', + }, + { + id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', + name: '10:panel_10', + type: 'search', + }, + { + id: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', + name: '11:panel_11', + type: 'map', + }, + { + id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60', + name: 'a71cf076-6895-491c-8878-63592e429ed5:panel_a71cf076-6895-491c-8878-63592e429ed5', + type: 'visualization', + }, + { + id: 'c3378480-f5ea-11eb-a78e-83aac3c38a60', + name: 'adc0a2f4-481c-45eb-b422-0ea59a3e5163:panel_adc0a2f4-481c-45eb-b422-0ea59a3e5163', + type: 'visualization', + }, + { + id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60', + name: '7077b79f-2a99-4fcb-bbd4-456982843278:panel_7077b79f-2a99-4fcb-bbd4-456982843278', + type: 'lens', + }, + { + id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60', + name: '19a3c101-ad2e-4421-a71b-a4734ec1f03e:panel_19a3c101-ad2e-4421-a71b-a4734ec1f03e', + type: 'lens', + }, + { + id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60', + name: '491469e7-7d24-4216-aeb3-bca00e5c8c1b:panel_491469e7-7d24-4216-aeb3-bca00e5c8c1b', + type: 'lens', + }, + { + id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60', + name: 'a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef:panel_a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef', + type: 'lens', + }, + { + id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60', + name: 'da51079b-952f-43dc-96e6-6f9415a3708b:panel_da51079b-952f-43dc-96e6-6f9415a3708b', + type: 'lens', + }, + { + id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60', + name: '64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b:panel_64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b', + type: 'lens', + }, + { + id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60', + name: 'bd330ede-2eef-4e2a-8100-22a21abf5038:panel_bd330ede-2eef-4e2a-8100-22a21abf5038', + type: 'lens', + }, + { + id: '03071e90-f5eb-11eb-a78e-83aac3c38a60', + name: 'b897d4be-cf83-46fb-a111-c7fbec9ef403:panel_b897d4be-cf83-46fb-a111-c7fbec9ef403', + type: 'lens', + }, + { + id: '06379e00-f5eb-11eb-a78e-83aac3c38a60', + name: 'e0f68f93-30f2-4da7-889a-6cd128a68d3f:panel_e0f68f93-30f2-4da7-889a-6cd128a68d3f', + type: 'lens', + }, + ], + type: 'dashboard', + updated_at: '2021-08-05T12:45:46.525Z', + version: 'WzIzOSwxXQ==', }, ]; diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index b15e1fc011b9..9324978b227d 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 2ab8037639f8..2b71d1882b0c 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -43,6 +43,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` labelType="label" > { this.onColorChange( { @@ -120,6 +121,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -144,6 +146,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -168,6 +171,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -220,6 +224,7 @@ export class ColorFormatEditor extends DefaultFormatEditor items.length > 1, + 'data-test-subj': 'colorEditorRemoveColor', }, ], }, @@ -229,7 +234,12 @@ export class ColorFormatEditor extends DefaultFormatEditor - + { return { @@ -126,6 +127,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< isInvalid={!!error} > { return { diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index 4d42e3848d3c..8b59c0da1016 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -43,6 +43,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` labelType="label" > { diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap index fce51e8fa387..7d8ab5e682a3 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -43,6 +43,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` labelType="label" > { this.onLookupChange( { @@ -105,6 +106,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor { this.onLookupChange( { @@ -136,6 +138,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor items.length > 1, }, ], @@ -147,7 +150,12 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor - + { if (e.target.checkValidity()) { this.onChange({ diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json index 5450ae74a91a..e5caf463835d 100644 --- a/src/plugins/index_pattern_field_editor/tsconfig.json +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 16afcb3599fe..2a719e98bea3 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx index 867fe13aee08..81ff7a5aa47f 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx +++ b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx @@ -24,8 +24,7 @@ export const getInputControlVisRenderer: ( if (!registeredController) { const { createInputControlVisController } = await import('./vis_controller'); - const Controller = createInputControlVisController(deps, handlers); - registeredController = new Controller(domNode); + registeredController = createInputControlVisController(deps, handlers, domNode); inputControlVisRegistry.set(domNode, registeredController); handlers.onDestroy(() => { diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx index 566f9035fe77..bb09a90bb9dd 100644 --- a/src/plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -24,18 +24,17 @@ import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; import { InputControlVisParams } from './types'; -export type InputControlVisControllerType = InstanceType< - ReturnType ->; +export type InputControlVisControllerType = ReturnType; export const createInputControlVisController = ( deps: InputControlVisDependencies, - handlers: IInterpreterRenderHandlers + handlers: IInterpreterRenderHandlers, + el: Element ) => { - return class InputControlVisController { - private I18nContext?: I18nStart['Context']; - private _isLoaded = false; + let I18nContext: I18nStart['Context'] | undefined; + let isLoaded = false; + return new (class InputControlVisController { controls: Array; queryBarUpdateHandler: () => void; filterManager: FilterManager; @@ -43,7 +42,7 @@ export const createInputControlVisController = ( timeFilterSubscription: Subscription; visParams?: InputControlVisParams; - constructor(public el: Element) { + constructor() { this.controls = []; this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); @@ -56,21 +55,21 @@ export const createInputControlVisController = ( .getTimeUpdate$() .subscribe(() => { if (this.visParams?.useTimeFilter) { - this._isLoaded = false; + isLoaded = false; } }); } async render(visParams: InputControlVisParams) { - if (!this.I18nContext) { + if (!I18nContext) { const [{ i18n }] = await deps.core.getStartServices(); - this.I18nContext = i18n.Context; + I18nContext = i18n.Context; } - if (!this._isLoaded || !isEqual(visParams, this.visParams)) { + if (!isLoaded || !isEqual(visParams, this.visParams)) { this.visParams = visParams; this.controls = []; this.controls = await this.initControls(visParams); - this._isLoaded = true; + isLoaded = true; } this.drawVis(); } @@ -78,17 +77,17 @@ export const createInputControlVisController = ( destroy() { this.updateSubsciption.unsubscribe(); this.timeFilterSubscription.unsubscribe(); - unmountComponentAtNode(this.el); + unmountComponentAtNode(el); this.controls.forEach((control) => control.destroy()); } drawVis = () => { - if (!this.I18nContext) { + if (!I18nContext) { throw new Error('no i18n context found'); } render( - + - , - this.el + , + el ); }; @@ -235,5 +234,5 @@ export const createInputControlVisController = ( await this.controls[controlIndex].fetch(query); this.drawVis(); }; - }; + })(); }; diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json index bef7bc394a6c..5e53199bb1e6 100644 --- a/src/plugins/input_control_vis/tsconfig.json +++ b/src/plugins/input_control_vis/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json index 2a9c41464532..4554a90821d4 100644 --- a/src/plugins/inspector/tsconfig.json +++ b/src/plugins/inspector/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/interactive_setup/tsconfig.json b/src/plugins/interactive_setup/tsconfig.json index 530e01a034b0..6ebeff836f69 100644 --- a/src/plugins/interactive_setup/tsconfig.json +++ b/src/plugins/interactive_setup/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_legacy/tsconfig.json b/src/plugins/kibana_legacy/tsconfig.json index 709036c9e82f..17f1a70838fd 100644 --- a/src/plugins/kibana_legacy/tsconfig.json +++ b/src/plugins/kibana_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index ac3ac109cb35..fd9002d39ee8 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index d4fb5a708e44..230d31ac9e0b 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -11,6 +11,9 @@ exports[`is rendered 1`] = ` onChange={[Function]} options={ Object { + "fontFamily": "Roboto Mono", + "fontSize": 12, + "lineHeight": 21, "matchBrackets": "never", "minimap": Object { "enabled": false, 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 6b02d8d15961..e233b4468b0c 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -193,6 +193,9 @@ export class CodeEditor extends React.Component { wordWrap: 'on', wrappingIndent: 'indent', matchBrackets: 'never', + fontFamily: 'Roboto Mono', + fontSize: 12, + lineHeight: 21, ...options, }} /> diff --git a/src/plugins/kibana_react/tsconfig.json b/src/plugins/kibana_react/tsconfig.json index eb9a24ca141f..3f6dd8fd280b 100644 --- a/src/plugins/kibana_react/tsconfig.json +++ b/src/plugins/kibana_react/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index ee07dfe589e4..e57d6e25db8c 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts index fc48ab119b02..efcdc7cbc46a 100644 --- a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts +++ b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts @@ -7,7 +7,8 @@ */ import { mergeWith } from 'lodash'; -import { MigrateFunctionsObject, MigrateFunction, SerializableState } from './types'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunctionsObject, MigrateFunction } from './types'; export const mergeMigrationFunctionMaps = ( obj1: MigrateFunctionsObject, @@ -17,7 +18,7 @@ export const mergeMigrationFunctionMaps = ( if (!srcValue || !objValue) { return srcValue || objValue; } - return (state: SerializableState) => objValue(srcValue(state)); + return (state: SerializableRecord) => objValue(srcValue(state)); }; return mergeWith({ ...obj1 }, obj2, customizer); diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts index 32fb652d4163..2a857b821ace 100644 --- a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts @@ -6,19 +6,20 @@ * Side Public License, v 1. */ -import { SerializableState, MigrateFunction } from './types'; +import { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunction } from './types'; import { migrateToLatest } from './migrate_to_latest'; -interface StateV1 extends SerializableState { +interface StateV1 extends SerializableRecord { name: string; } -interface StateV2 extends SerializableState { +interface StateV2 extends SerializableRecord { firstName: string; lastName: string; } -interface StateV3 extends SerializableState { +interface StateV3 extends SerializableRecord { firstName: string; lastName: string; isAdmin: boolean; diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts index 6f81d0a7b9b6..9481e333819b 100644 --- a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts @@ -7,9 +7,10 @@ */ import { compare } from 'semver'; -import { SerializableState, VersionedState, MigrateFunctionsObject } from './types'; +import { SerializableRecord } from '@kbn/utility-types'; +import { VersionedState, MigrateFunctionsObject } from './types'; -export function migrateToLatest( +export function migrateToLatest( migrations: MigrateFunctionsObject, { state, version: oldVersion }: VersionedState ): S { diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts index a2d1751297a9..6fea0a3a4eab 100644 --- a/src/plugins/kibana_utils/common/persistable_state/types.ts +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -6,18 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from '../../../../core/types'; -/** - * Serializable state is something is a POJO JavaScript object that can be - * serialized to a JSON string. - */ -export type SerializableState = { - [key: string]: Serializable; -}; -export type SerializableValue = string | number | boolean | null | undefined | SerializableState; -export type Serializable = SerializableValue | SerializableValue[]; - /** * Versioned state is a POJO JavaScript object that can be serialized to JSON, * and which also contains the version information. The version is stored in @@ -35,7 +26,7 @@ export type Serializable = SerializableValue | SerializableValue[]; * }; * ``` */ -export interface VersionedState { +export interface VersionedState { version: string; state: S; } @@ -50,7 +41,7 @@ export interface VersionedState * * @todo Maybe rename it to `PersistableStateItem`? */ -export interface PersistableState

{ +export interface PersistableState

{ /** * Function which reports telemetry information. This function is essentially * a "reducer" - it receives the existing "stats" object and returns an @@ -101,8 +92,8 @@ export interface PersistableState

}; export type MigrateFunction< - FromVersion extends SerializableState = SerializableState, - ToVersion extends SerializableState = SerializableState + FromVersion extends SerializableRecord = SerializableRecord, + ToVersion extends SerializableRecord = SerializableRecord > = (state: FromVersion) => ToVersion; /** @@ -111,21 +102,21 @@ export type MigrateFunction< * @param version */ export type PersistableStateMigrateFn = ( - state: SerializableState, + state: SerializableRecord, version: string -) => SerializableState; +) => SerializableRecord; /** * @todo Shall we remove this? */ -export type PersistableStateDefinition

= Partial< +export type PersistableStateDefinition

= Partial< PersistableState

>; /** * @todo Add description. */ -export interface PersistableStateService

{ +export interface PersistableStateService

{ /** * Function which reports telemetry information. This function is essentially * a "reducer" - it receives the existing "stats" object and returns an diff --git a/src/plugins/kibana_utils/public/ui/configurable.ts b/src/plugins/kibana_utils/public/ui/configurable.ts index 70f91915771f..cf02b9e977d1 100644 --- a/src/plugins/kibana_utils/public/ui/configurable.ts +++ b/src/plugins/kibana_utils/public/ui/configurable.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { UiComponent } from '../../common/ui/ui_component'; -import { SerializableState } from '../../common'; /** * Represents something that can be configured by user using UI. */ export interface Configurable< - Config extends SerializableState = SerializableState, + Config extends SerializableRecord = SerializableRecord, Context = object > { /** @@ -36,7 +36,7 @@ export interface Configurable< * Props provided to `CollectConfig` component on every re-render. */ export interface CollectConfigProps< - Config extends SerializableState = SerializableState, + Config extends SerializableRecord = SerializableRecord, Context = object > { /** diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index ae5e9b90af80..0538b145a5d6 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json index ec006d492499..2f071b5ba6c5 100644 --- a/src/plugins/legacy_export/tsconfig.json +++ b/src/plugins/legacy_export/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 7dbf5e288801..f56b2885be09 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; -export interface ManagementAppLocatorParams extends SerializableState { +export interface ManagementAppLocatorParams extends SerializableRecord { sectionId: string; appId?: string; } diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index ba3661666631..beef79c9affd 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/maps_ems/common/ems_defaults.ts b/src/plugins/maps_ems/common/ems_defaults.ts index a494386b100b..5b5e8669cffa 100644 --- a/src/plugins/maps_ems/common/ems_defaults.ts +++ b/src/plugins/maps_ems/common/ems_defaults.ts @@ -9,7 +9,7 @@ // Default config for the elastic hosted EMS endpoints export const DEFAULT_EMS_FILE_API_URL = 'https://vector.maps.elastic.co'; export const DEFAULT_EMS_TILE_API_URL = 'https://tiles.maps.elastic.co'; -export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.13'; +export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.14'; export const DEFAULT_EMS_FONT_LIBRARY_URL = 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'; diff --git a/src/plugins/maps_ems/tsconfig.json b/src/plugins/maps_ems/tsconfig.json index b85c3da66b83..dd579b47f310 100644 --- a/src/plugins/maps_ems/tsconfig.json +++ b/src/plugins/maps_ems/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json index f757e35f785a..b6fcb9345b1c 100644 --- a/src/plugins/maps_legacy/tsconfig.json +++ b/src/plugins/maps_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json index 07cfe10d7d81..1b7e87aaba41 100644 --- a/src/plugins/navigation/tsconfig.json +++ b/src/plugins/navigation/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 18e6f2de1bc6..1388e1108725 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/presentation_util/public/components/fixtures/flights.ts b/src/plugins/presentation_util/public/components/fixtures/flights.ts new file mode 100644 index 000000000000..19ac29226693 --- /dev/null +++ b/src/plugins/presentation_util/public/components/fixtures/flights.ts @@ -0,0 +1,1953 @@ +/* + * 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 const flights = [ + { + FlightNum: '9HY9SWR', + DestCountry: 'AU', + OriginWeather: 'Sunny', + OriginCityName: 'Frankfurt am Main', + AvgTicketPrice: 841.2656419677076, + DistanceMiles: 10247.856675613455, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Sydney Kingsford Smith International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 16492.32665375846, + timestamp: '2018-01-01T00:00:00', + DestLocation: { lat: '-33.94609833', lon: '151.177002' }, + DestAirportID: 'SYD', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 1030.7704158599038, + Origin: 'Frankfurt am Main Airport', + OriginLocation: { lat: '50.033333', lon: '8.570556' }, + DestRegion: 'SE-BD', + OriginAirportID: 'FRA', + OriginRegion: 'DE-HE', + DestCityName: 'Sydney', + FlightTimeHour: 17.179506930998397, + FlightDelayMin: 0, + }, + { + FlightNum: 'X98CCZO', + DestCountry: 'IT', + OriginWeather: 'Clear', + OriginCityName: 'Cape Town', + AvgTicketPrice: 882.9826615595518, + DistanceMiles: 5482.606664853586, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'ZA', + dayOfWeek: 0, + DistanceKilometers: 8823.40014044213, + timestamp: '2018-01-01T18:27:00', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 464.3894810759016, + Origin: 'Cape Town International Airport', + OriginLocation: { lat: '-33.96480179', lon: '18.60169983' }, + DestRegion: 'IT-34', + OriginAirportID: 'CPT', + OriginRegion: 'SE-BD', + DestCityName: 'Venice', + FlightTimeHour: 7.73982468459836, + FlightDelayMin: 0, + }, + { + FlightNum: 'UFK2WIZ', + DestCountry: 'IT', + OriginWeather: 'Rain', + OriginCityName: 'Venice', + AvgTicketPrice: 190.6369038508356, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T17:11:14', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Venice Marco Polo Airport', + OriginLocation: { lat: '45.505299', lon: '12.3519' }, + DestRegion: 'IT-34', + OriginAirportID: 'VE05', + OriginRegion: 'IT-34', + DestCityName: 'Venice', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: 'EAYQW69', + DestCountry: 'IT', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Naples', + AvgTicketPrice: 181.69421554118, + DistanceMiles: 345.31943877289535, + FlightDelay: true, + DestWeather: 'Clear', + Dest: "Treviso-Sant'Angelo Airport", + FlightDelayType: 'Weather Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 555.7377668725265, + timestamp: '2018-01-01T10:33:28', + DestLocation: { lat: '45.648399', lon: '12.1944' }, + DestAirportID: 'TV01', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 222.74905899019436, + Origin: 'Naples International Airport', + OriginLocation: { lat: '40.886002', lon: '14.2908' }, + DestRegion: 'IT-34', + OriginAirportID: 'NA01', + OriginRegion: 'IT-72', + DestCityName: 'Treviso', + FlightTimeHour: 3.712484316503239, + FlightDelayMin: 180, + }, + { + FlightNum: '58U013N', + DestCountry: 'CN', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Mexico City', + AvgTicketPrice: 730.041778346198, + DistanceMiles: 8300.428124665925, + FlightDelay: false, + DestWeather: 'Clear', + Dest: "Xi'an Xianyang International Airport", + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 13358.24419986236, + timestamp: '2018-01-01T05:13:00', + DestLocation: { lat: '34.447102', lon: '108.751999' }, + DestAirportID: 'XIY', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 785.7790705801389, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: "Xi'an", + FlightTimeHour: 13.096317843002314, + FlightDelayMin: 0, + }, + { + FlightNum: 'XEJ78I2', + DestCountry: 'IT', + OriginWeather: 'Rain', + OriginCityName: 'Edmonton', + AvgTicketPrice: 418.1520890531832, + DistanceMiles: 4891.315227492962, + FlightDelay: false, + DestWeather: 'Thunder & Lightning', + Dest: 'Genoa Cristoforo Colombo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CA', + dayOfWeek: 0, + DistanceKilometers: 7871.808813474433, + timestamp: '2018-01-01T01:43:03', + DestLocation: { lat: '44.4133', lon: '8.8375' }, + DestAirportID: 'GE01', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 393.5904406737217, + Origin: 'Edmonton International Airport', + OriginLocation: { lat: '53.30970001', lon: '-113.5800018' }, + DestRegion: 'IT-42', + OriginAirportID: 'CYEG', + OriginRegion: 'CA-AB', + DestCityName: 'Genova', + FlightTimeHour: 6.5598406778953615, + FlightDelayMin: 0, + }, + { + FlightNum: 'EVARI8I', + DestCountry: 'CH', + OriginWeather: 'Clear', + OriginCityName: 'Zurich', + AvgTicketPrice: 180.24681638061213, + DistanceMiles: 0.0, + FlightDelay: true, + DestWeather: 'Hail', + Dest: 'Zurich Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T13:49:53', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 300.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 5.0, + FlightDelayMin: 300, + }, + { + FlightNum: '1IRBW25', + DestCountry: 'CA', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Rome', + AvgTicketPrice: 585.1843103083941, + DistanceMiles: 4203.1829639346715, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6764.367283910481, + timestamp: '2018-01-01T04:54:59', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 614.9424803554983, + Origin: 'Ciampino___G. B. Pastine International Airport', + OriginLocation: { lat: '41.7994', lon: '12.5949' }, + DestRegion: 'CA-ON', + OriginAirportID: 'RM12', + OriginRegion: 'IT-62', + DestCityName: 'Ottawa', + FlightTimeHour: 10.249041339258305, + FlightDelayMin: 0, + }, + { + FlightNum: 'M05KE88', + DestCountry: 'IN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Milan', + AvgTicketPrice: 960.8697358054351, + DistanceMiles: 4377.166776556647, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'Rajiv Gandhi International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7044.367088850781, + timestamp: '2018-01-01T12:09:35', + DestLocation: { lat: '17.23131752', lon: '78.42985535' }, + DestAirportID: 'HYD', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 602.0305907375651, + Origin: 'Milano Linate Airport', + OriginLocation: { lat: '45.445099', lon: '9.27674' }, + DestRegion: 'SE-BD', + OriginAirportID: 'MI11', + OriginRegion: 'IT-25', + DestCityName: 'Hyderabad', + FlightTimeHour: 10.033843178959419, + FlightDelayMin: 15, + }, + { + FlightNum: 'SNI3M1Z', + DestCountry: 'IT', + OriginWeather: 'Cloudy', + OriginCityName: 'Moscow', + AvgTicketPrice: 296.8777725965789, + DistanceMiles: 1303.5538675692512, + FlightDelay: false, + DestWeather: 'Rain', + Dest: "Treviso-Sant'Angelo Airport", + FlightDelayType: 'No Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 2097.866595449369, + timestamp: '2018-01-01T12:09:35', + DestLocation: { lat: '45.648399', lon: '12.1944' }, + DestAirportID: 'TV01', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 174.82221628744742, + Origin: 'Sheremetyevo International Airport', + OriginLocation: { lat: '55.972599', lon: '37.4146' }, + DestRegion: 'IT-34', + OriginAirportID: 'SVO', + OriginRegion: 'RU-MOS', + DestCityName: 'Treviso', + FlightTimeHour: 2.9137036047907903, + FlightDelayMin: 0, + }, + { + FlightNum: 'JQ2XXQ5', + DestCountry: 'FI', + OriginWeather: 'Rain', + OriginCityName: 'Albuquerque', + AvgTicketPrice: 906.4379477399872, + DistanceMiles: 5313.8222112173335, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Helsinki Vantaa Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8551.76789268935, + timestamp: '2018-01-01T22:06:14', + DestLocation: { lat: '60.31719971', lon: '24.9633007' }, + DestAirportID: 'HEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 503.04517015819704, + Origin: 'Albuquerque International Sunport Airport', + OriginLocation: { lat: '35.040199', lon: '-106.609001' }, + DestRegion: 'FI-ES', + OriginAirportID: 'ABQ', + OriginRegion: 'US-NM', + DestCityName: 'Helsinki', + FlightTimeHour: 8.384086169303284, + FlightDelayMin: 0, + }, + { + FlightNum: 'V30ITD0', + DestCountry: 'AT', + OriginWeather: 'Rain', + OriginCityName: 'Venice', + AvgTicketPrice: 704.4637710312036, + DistanceMiles: 268.99172653633303, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Vienna International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 432.90022115088834, + timestamp: '2018-01-01T11:52:34', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 36.07501842924069, + Origin: 'Venice Marco Polo Airport', + OriginLocation: { lat: '45.505299', lon: '12.3519' }, + DestRegion: 'AT-9', + OriginAirportID: 'VE05', + OriginRegion: 'IT-34', + DestCityName: 'Vienna', + FlightTimeHour: 0.6012503071540115, + FlightDelayMin: 0, + }, + { + FlightNum: 'P0WMFH7', + DestCountry: 'CN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Mexico City', + AvgTicketPrice: 922.499077027416, + DistanceMiles: 8025.381414737853, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 12915.599427519877, + timestamp: '2018-01-01T02:13:46', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 679.7683909220988, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Shanghai', + FlightTimeHour: 11.32947318203498, + FlightDelayMin: 0, + }, + { + FlightNum: 'VT9O2KD', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Naples', + AvgTicketPrice: 374.9592762864519, + DistanceMiles: 4311.560440686985, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6938.783925856956, + timestamp: '2018-01-01T14:21:13', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 330.41828218366453, + Origin: 'Naples International Airport', + OriginLocation: { lat: '40.886002', lon: '14.2908' }, + DestRegion: 'CA-ON', + OriginAirportID: 'NA01', + OriginRegion: 'IT-72', + DestCityName: 'Ottawa', + FlightTimeHour: 5.506971369727742, + FlightDelayMin: 0, + }, + { + FlightNum: 'NRHSVG8', + DestCountry: 'PR', + OriginWeather: 'Cloudy', + OriginCityName: 'Rome', + AvgTicketPrice: 552.9173708459598, + DistanceMiles: 4806.775668847457, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Luis Munoz Marin International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7735.755582005642, + timestamp: '2018-01-01T17:42:53', + DestLocation: { lat: '18.43939972', lon: '-66.00180054' }, + DestAirportID: 'SJU', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 407.1450306318759, + Origin: 'Ciampino___G. B. Pastine International Airport', + OriginLocation: { lat: '41.7994', lon: '12.5949' }, + DestRegion: 'PR-U-A', + OriginAirportID: 'RM12', + OriginRegion: 'IT-62', + DestCityName: 'San Juan', + FlightTimeHour: 6.7857505105312645, + FlightDelayMin: 0, + }, + { + FlightNum: 'YIPS2BZ', + DestCountry: 'DE', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Chengdu', + AvgTicketPrice: 566.4875569256166, + DistanceMiles: 4896.74792596565, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Cologne Bonn Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 7880.551894165264, + timestamp: '2018-01-01T19:55:32', + DestLocation: { lat: '50.86589813', lon: '7.142739773' }, + DestAirportID: 'CGN', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 656.7126578471053, + Origin: 'Chengdu Shuangliu International Airport', + OriginLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestRegion: 'DE-NW', + OriginAirportID: 'CTU', + OriginRegion: 'SE-BD', + DestCityName: 'Cologne', + FlightTimeHour: 10.945210964118422, + FlightDelayMin: 0, + }, + { + FlightNum: 'C7IBZ42', + DestCountry: 'IT', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Mexico City', + AvgTicketPrice: 989.9527866266118, + DistanceMiles: 6244.404143498341, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 10049.394341914194, + timestamp: '2018-01-01T07:49:27', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 773.0303339933996, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'IT-34', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Venice', + FlightTimeHour: 12.883838899889993, + FlightDelayMin: 0, + }, + { + FlightNum: '7TTZM4I', + DestCountry: 'AR', + OriginWeather: 'Rain', + OriginCityName: 'Cleveland', + AvgTicketPrice: 569.6132552035547, + DistanceMiles: 5450.245542110189, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'Ministro Pistarini International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8771.31996172178, + timestamp: '2018-01-01T01:30:47', + DestLocation: { lat: '-34.8222', lon: '-58.5358' }, + DestAirportID: 'EZE', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 704.7169201324446, + Origin: 'Cleveland Hopkins International Airport', + OriginLocation: { lat: '41.4117012', lon: '-81.84980011' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CLE', + OriginRegion: 'US-OH', + DestCityName: 'Buenos Aires', + FlightTimeHour: 11.745282002207409, + FlightDelayMin: 30, + }, + { + FlightNum: 'CSW89S3', + DestCountry: 'CN', + OriginWeather: 'Hail', + OriginCityName: 'Olenegorsk', + AvgTicketPrice: 277.4297073844173, + DistanceMiles: 4202.458848620224, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 6763.2019332738655, + timestamp: '2018-01-01T07:58:17', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 355.95799648809816, + Origin: 'Olenya Air Base', + OriginLocation: { lat: '68.15180206', lon: '33.46390152' }, + DestRegion: 'SE-BD', + OriginAirportID: 'XLMO', + OriginRegion: 'RU-MUR', + DestCityName: 'Shanghai', + FlightTimeHour: 5.932633274801636, + FlightDelayMin: 0, + }, + { + FlightNum: 'RBFKZBX', + DestCountry: 'IN', + OriginWeather: 'Cloudy', + OriginCityName: 'Casper', + AvgTicketPrice: 772.1008456460212, + DistanceMiles: 7507.304095087099, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Indira Gandhi International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 12081.834801603853, + timestamp: '2018-01-01T00:02:06', + DestLocation: { lat: '28.5665', lon: '77.103104' }, + DestAirportID: 'DEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 875.1146751002408, + Origin: 'Casper-Natrona County International Airport', + OriginLocation: { lat: '42.90800095', lon: '-106.4639969' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CPR', + OriginRegion: 'US-WY', + DestCityName: 'New Delhi', + FlightTimeHour: 14.585244585004013, + FlightDelayMin: 120, + }, + { + FlightNum: 'R43CELD', + DestCountry: 'US', + OriginWeather: 'Cloudy', + OriginCityName: 'Erie', + AvgTicketPrice: 167.59992219374266, + DistanceMiles: 965.1786928006221, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Wichita Mid Continent Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 1553.3045381865245, + timestamp: '2018-01-01T01:08:20', + DestLocation: { lat: '37.64989853', lon: '-97.43309784' }, + DestAirportID: 'ICT', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 373.96688277078687, + Origin: 'Erie International Tom Ridge Field', + OriginLocation: { lat: '42.08312701', lon: '-80.17386675' }, + DestRegion: 'US-KS', + OriginAirportID: 'ERI', + OriginRegion: 'US-PA', + DestCityName: 'Wichita', + FlightTimeHour: 6.232781379513114, + FlightDelayMin: 300, + }, + { + FlightNum: '1TJWK8F', + DestCountry: 'CA', + OriginWeather: 'Clear', + OriginCityName: 'Newark', + AvgTicketPrice: 253.21006490337038, + DistanceMiles: 328.5065863946441, + FlightDelay: true, + DestWeather: 'Hail', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 528.6801037747022, + timestamp: '2018-01-01T01:08:20', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 130.66770029036172, + Origin: 'Newark Liberty International Airport', + OriginLocation: { lat: '40.69250107', lon: '-74.16870117' }, + DestRegion: 'CA-ON', + OriginAirportID: 'EWR', + OriginRegion: 'US-NJ', + DestCityName: 'Ottawa', + FlightTimeHour: 2.177795004839362, + FlightDelayMin: 90, + }, + { + FlightNum: 'UDTSN13', + DestCountry: 'JP', + OriginWeather: 'Sunny', + OriginCityName: 'Copenhagen', + AvgTicketPrice: 917.2476203642742, + DistanceMiles: 5354.622537746889, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Itami Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DK', + dayOfWeek: 0, + DistanceKilometers: 8617.42965338773, + timestamp: '2018-01-01T07:48:35', + DestLocation: { lat: '34.78549957', lon: '135.4380035' }, + DestAirportID: 'ITM', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 574.4953102258486, + Origin: 'Copenhagen Kastrup Airport', + OriginLocation: { lat: '55.61790085', lon: '12.65600014' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CPH', + OriginRegion: 'DK-84', + DestCityName: 'Osaka', + FlightTimeHour: 9.574921837097476, + FlightDelayMin: 0, + }, + { + FlightNum: '7Y08OAU', + DestCountry: 'AT', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Seattle', + AvgTicketPrice: 451.5911757026697, + DistanceMiles: 5403.40296642641, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Vienna International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8695.934143600545, + timestamp: '2018-01-01T18:57:21', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 579.728942906703, + Origin: 'Seattle Tacoma International Airport', + OriginLocation: { lat: '47.44900131', lon: '-122.3089981' }, + DestRegion: 'AT-9', + OriginAirportID: 'SEA', + OriginRegion: 'US-WA', + DestCityName: 'Vienna', + FlightTimeHour: 9.66214904844505, + FlightDelayMin: 0, + }, + { + FlightNum: 'MH65PPZ', + DestCountry: 'FR', + OriginWeather: 'Rain', + OriginCityName: 'Berlin', + AvgTicketPrice: 307.0672008820741, + DistanceMiles: 529.8263707088325, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Charles de Gaulle International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 852.6728907420354, + timestamp: '2018-01-01T13:18:25', + DestLocation: { lat: '49.01279831', lon: '2.549999952' }, + DestAirportID: 'CDG', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 50.15722886717855, + Origin: 'Berlin-Tegel Airport', + OriginLocation: { lat: '52.5597', lon: '13.2877' }, + DestRegion: 'FR-J', + OriginAirportID: 'TXL', + OriginRegion: 'DE-BE', + DestCityName: 'Paris', + FlightTimeHour: 0.8359538144529759, + FlightDelayMin: 0, + }, + { + FlightNum: '7SFSTEH', + DestCountry: 'JP', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Manchester', + AvgTicketPrice: 268.24159591388866, + DistanceMiles: 5900.673562063734, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Narita International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 9496.213593065899, + timestamp: '2018-01-01T08:20:35', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 527.5674218369944, + Origin: 'Manchester Airport', + OriginLocation: { lat: '53.35369873', lon: '-2.274950027' }, + DestRegion: 'SE-BD', + OriginAirportID: 'MAN', + OriginRegion: 'GB-ENG', + DestCityName: 'Tokyo', + FlightTimeHour: 8.792790363949907, + FlightDelayMin: 0, + }, + { + FlightNum: '430NKQO', + DestCountry: 'JP', + OriginWeather: 'Rain', + OriginCityName: 'Helsinki', + AvgTicketPrice: 975.8126321392289, + DistanceMiles: 4800.213800795046, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Itami Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'FI', + dayOfWeek: 0, + DistanceKilometers: 7725.195279026703, + timestamp: '2018-01-01T15:38:32', + DestLocation: { lat: '34.78549957', lon: '135.4380035' }, + DestAirportID: 'ITM', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 386.2597639513352, + Origin: 'Helsinki Vantaa Airport', + OriginLocation: { lat: '60.31719971', lon: '24.9633007' }, + DestRegion: 'SE-BD', + OriginAirportID: 'HEL', + OriginRegion: 'FI-ES', + DestCityName: 'Osaka', + FlightTimeHour: 6.437662732522253, + FlightDelayMin: 0, + }, + { + FlightNum: '3YAQM9U', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Phoenix', + AvgTicketPrice: 134.21454568995148, + DistanceMiles: 304.2189898030449, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'San Diego International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 489.59300592559157, + timestamp: '2018-01-01T03:08:45', + DestLocation: { lat: '32.73360062', lon: '-117.1900024' }, + DestAirportID: 'SAN', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 24.47965029627958, + Origin: 'Phoenix Sky Harbor International Airport', + OriginLocation: { lat: '33.43429947', lon: '-112.012001' }, + DestRegion: 'US-CA', + OriginAirportID: 'PHX', + OriginRegion: 'US-AZ', + DestCityName: 'San Diego', + FlightTimeHour: 0.4079941716046597, + FlightDelayMin: 0, + }, + { + FlightNum: 'HX0WBLI', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Chitose / Tomakomai', + AvgTicketPrice: 988.8975638746068, + DistanceMiles: 5650.511340218511, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Verona Villafranca Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 9093.616522312619, + timestamp: '2018-01-01T01:16:59', + DestLocation: { lat: '45.395699', lon: '10.8885' }, + DestAirportID: 'VR10', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 568.3510326445387, + Origin: 'New Chitose Airport', + OriginLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestRegion: 'IT-34', + OriginAirportID: 'CTS', + OriginRegion: 'SE-BD', + DestCityName: 'Verona', + FlightTimeHour: 9.472517210742312, + FlightDelayMin: 0, + }, + { + FlightNum: 'BSVTVSA', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Tulsa', + AvgTicketPrice: 511.0672203518154, + DistanceMiles: 5028.070244660456, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8091.894679822838, + timestamp: '2018-01-01T18:00:59', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 425.8891936748862, + Origin: 'Tulsa International Airport', + OriginLocation: { lat: '36.19839859', lon: '-95.88809967' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'TUL', + OriginRegion: 'US-OK', + DestCityName: 'Zurich', + FlightTimeHour: 7.09815322791477, + FlightDelayMin: 0, + }, + { + FlightNum: 'OODIP58', + DestCountry: 'CN', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Abu Dhabi', + AvgTicketPrice: 252.9119662217096, + DistanceMiles: 3032.4467769272865, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Chengdu Shuangliu International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 4880.250025767267, + timestamp: '2018-01-01T12:05:14', + DestLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestAirportID: 'CTU', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 490.3500017178178, + Origin: 'Abu Dhabi International Airport', + OriginLocation: { lat: '24.43300056', lon: '54.65110016' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AUH', + OriginRegion: 'SE-BD', + DestCityName: 'Chengdu', + FlightTimeHour: 8.172500028630298, + FlightDelayMin: 165, + }, + { + FlightNum: 'J6XATW0', + DestCountry: 'JP', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Catania', + AvgTicketPrice: 572.072088442, + DistanceMiles: 6298.772997224989, + FlightDelay: true, + DestWeather: 'Damaging Wind', + Dest: 'Narita International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 10136.892530446054, + timestamp: '2018-01-01T04:18:52', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 979.7410442038378, + Origin: 'Catania-Fontanarossa Airport', + OriginLocation: { lat: '37.466801', lon: '15.0664' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CT03', + OriginRegion: 'IT-82', + DestCityName: 'Tokyo', + FlightTimeHour: 16.329017403397295, + FlightDelayMin: 135, + }, + { + FlightNum: 'JB2BVNL', + DestCountry: 'JP', + OriginWeather: 'Cloudy', + OriginCityName: 'Louisville', + AvgTicketPrice: 676.8834852133116, + DistanceMiles: 6553.342465155623, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Narita International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 10546.582376243412, + timestamp: '2018-01-01T08:31:08', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 963.3273125888152, + Origin: 'Louisville International Standiford Field', + OriginLocation: { lat: '38.1744', lon: '-85.736' }, + DestRegion: 'SE-BD', + OriginAirportID: 'SDF', + OriginRegion: 'US-KY', + DestCityName: 'Tokyo', + FlightTimeHour: 16.055455209813587, + FlightDelayMin: 210, + }, + { + FlightNum: '8SHQI41', + DestCountry: 'US', + OriginWeather: 'Cloudy', + OriginCityName: 'Spokane', + AvgTicketPrice: 131.81910243159925, + DistanceMiles: 1228.3139778743412, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Wichita Mid Continent Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 1976.7797304082037, + timestamp: '2018-01-01T10:36:25', + DestLocation: { lat: '37.64989853', lon: '-97.43309784' }, + DestAirportID: 'ICT', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 141.19855217201456, + Origin: 'Spokane International Airport', + OriginLocation: { lat: '47.61989975', lon: '-117.5339966' }, + DestRegion: 'US-KS', + OriginAirportID: 'GEG', + OriginRegion: 'US-WA', + DestCityName: 'Wichita', + FlightTimeHour: 2.3533092028669094, + FlightDelayMin: 0, + }, + { + FlightNum: 'IK1PCVN', + DestCountry: 'RU', + OriginWeather: 'Clear', + OriginCityName: 'Portland', + AvgTicketPrice: 470.6837477903439, + DistanceMiles: 5339.976001314777, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Sheremetyevo International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8593.858337859929, + timestamp: '2018-01-01T14:04:21', + DestLocation: { lat: '55.972599', lon: '37.4146' }, + DestAirportID: 'SVO', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 716.1548614883274, + Origin: 'Portland International Airport', + OriginLocation: { lat: '45.58869934', lon: '-122.5979996' }, + DestRegion: 'RU-MOS', + OriginAirportID: 'PDX', + OriginRegion: 'US-OR', + DestCityName: 'Moscow', + FlightTimeHour: 11.93591435813879, + FlightDelayMin: 0, + }, + { + FlightNum: 'U8SLHLH', + DestCountry: 'CO', + OriginWeather: 'Hail', + OriginCityName: 'Jebel Ali', + AvgTicketPrice: 547.6349165149929, + DistanceMiles: 8477.112527527572, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'El Dorado International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 13642.590183501334, + timestamp: '2018-01-01T04:49:05', + DestLocation: { lat: '4.70159', lon: '-74.1469' }, + DestAirportID: 'BOG', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 1064.4707273929525, + Origin: 'Al Maktoum International Airport', + OriginLocation: { lat: '24.896356', lon: '55.161389' }, + DestRegion: 'SE-BD', + OriginAirportID: 'DWC', + OriginRegion: 'SE-BD', + DestCityName: 'Bogota', + FlightTimeHour: 17.741178789882543, + FlightDelayMin: 90, + }, + { + FlightNum: 'LAEYSCF', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Greensboro', + AvgTicketPrice: 741.0225650260804, + DistanceMiles: 4416.574730450814, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Turin Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 7107.788043002635, + timestamp: '2018-01-01T12:56:09', + DestLocation: { lat: '45.200802', lon: '7.64963' }, + DestAirportID: 'TO11', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 394.8771135001464, + Origin: 'Piedmont Triad International Airport', + OriginLocation: { lat: '36.09780121', lon: '-79.93730164' }, + DestRegion: 'IT-21', + OriginAirportID: 'GSO', + OriginRegion: 'US-NC', + DestCityName: 'Torino', + FlightTimeHour: 6.58128522500244, + FlightDelayMin: 0, + }, + { + FlightNum: '17NH0H3', + DestCountry: 'CA', + OriginWeather: 'Cloudy', + OriginCityName: 'Treviso', + AvgTicketPrice: 404.731389894892, + DistanceMiles: 4614.531201475585, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Winnipeg / James Armstrong Richardson International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7426.368101907524, + timestamp: '2018-01-01T18:14:14', + DestLocation: { lat: '49.90999985', lon: '-97.23989868' }, + DestAirportID: 'YWG', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 353.6365762813107, + Origin: "Treviso-Sant'Angelo Airport", + OriginLocation: { lat: '45.648399', lon: '12.1944' }, + DestRegion: 'CA-MB', + OriginAirportID: 'TV01', + OriginRegion: 'IT-34', + DestCityName: 'Winnipeg', + FlightTimeHour: 5.893942938021845, + FlightDelayMin: 0, + }, + { + FlightNum: 'EA3F2E4', + DestCountry: 'CN', + OriginWeather: 'Clear', + OriginCityName: 'Catania', + AvgTicketPrice: 954.2386926506058, + DistanceMiles: 4927.508301892961, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Chengdu Shuangliu International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7930.055920601625, + timestamp: '2018-01-01T21:30:40', + DestLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestAirportID: 'CTU', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 566.4325657572589, + Origin: 'Catania-Fontanarossa Airport', + OriginLocation: { lat: '37.466801', lon: '15.0664' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CT03', + OriginRegion: 'IT-82', + DestCityName: 'Chengdu', + FlightTimeHour: 9.440542762620982, + FlightDelayMin: 0, + }, + { + FlightNum: 'HF9AP10', + DestCountry: 'US', + OriginWeather: 'Sunny', + OriginCityName: 'New York', + AvgTicketPrice: 231.96273728774017, + DistanceMiles: 2445.8920569992383, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'San Diego International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 3936.2817065793824, + timestamp: '2018-01-01T01:50:27', + DestLocation: { lat: '32.73360062', lon: '-117.1900024' }, + DestAirportID: 'SAN', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 262.4187804386255, + Origin: 'John F Kennedy International Airport', + OriginLocation: { lat: '40.63980103', lon: '-73.77890015' }, + DestRegion: 'US-CA', + OriginAirportID: 'JFK', + OriginRegion: 'US-NY', + DestCityName: 'San Diego', + FlightTimeHour: 4.3736463406437585, + FlightDelayMin: 0, + }, + { + FlightNum: '1Y5O0VK', + DestCountry: 'AR', + OriginWeather: 'Rain', + OriginCityName: 'Quito', + AvgTicketPrice: 453.7551255012854, + DistanceMiles: 2708.548842821117, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ministro Pistarini International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'EC', + dayOfWeek: 0, + DistanceKilometers: 4358.986828901108, + timestamp: '2018-01-01T23:43:02', + DestLocation: { lat: '-34.8222', lon: '-58.5358' }, + DestAirportID: 'EZE', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 242.16593493895047, + Origin: 'Mariscal Sucre International Airport', + OriginLocation: { lat: '-0.129166667', lon: '-78.3575' }, + DestRegion: 'AR-B', + OriginAirportID: 'UIO', + OriginRegion: 'SE-BD', + DestCityName: 'Buenos Aires', + FlightTimeHour: 4.036098915649174, + FlightDelayMin: 0, + }, + { + FlightNum: 'SLJ4EVS', + DestCountry: 'AT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Toronto', + AvgTicketPrice: 334.7392896190951, + DistanceMiles: 4329.582240070009, + FlightDelay: true, + DestWeather: 'Thunder & Lightning', + Dest: 'Vienna International Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'CA', + dayOfWeek: 0, + DistanceKilometers: 6967.787200563229, + timestamp: '2018-01-01T05:24:19', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 604.8698353272488, + Origin: 'Lester B. Pearson International Airport', + OriginLocation: { lat: '43.67720032', lon: '-79.63059998' }, + DestRegion: 'AT-9', + OriginAirportID: 'YYZ', + OriginRegion: 'CA-ON', + DestCityName: 'Vienna', + FlightTimeHour: 10.081163922120814, + FlightDelayMin: 195, + }, + { + FlightNum: 'ZTL6FPB', + DestCountry: 'US', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Bergamo', + AvgTicketPrice: 926.6665998573354, + DistanceMiles: 4643.872252383584, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Louisville International Standiford Field', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7473.587946140007, + timestamp: '2018-01-01T00:47:09', + DestLocation: { lat: '38.1744', lon: '-85.736' }, + DestAirportID: 'SDF', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 679.417086012728, + Origin: 'Il Caravaggio International Airport', + OriginLocation: { lat: '45.673901', lon: '9.70417' }, + DestRegion: 'US-KY', + OriginAirportID: 'BG01', + OriginRegion: 'IT-25', + DestCityName: 'Louisville', + FlightTimeHour: 11.323618100212133, + FlightDelayMin: 0, + }, + { + FlightNum: '46J5N4Y', + DestCountry: 'CA', + OriginWeather: 'Hail', + OriginCityName: 'London', + AvgTicketPrice: 664.6811922084918, + DistanceMiles: 3351.640388335116, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 5393.942349124789, + timestamp: '2018-01-01T17:22:39', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 283.8917025855152, + Origin: 'London Gatwick Airport', + OriginLocation: { lat: '51.14810181', lon: '-0.190277994' }, + DestRegion: 'CA-ON', + OriginAirportID: 'LGW', + OriginRegion: 'GB-ENG', + DestCityName: 'Ottawa', + FlightTimeHour: 4.731528376425254, + FlightDelayMin: 0, + }, + { + FlightNum: 'PVATFF1', + DestCountry: 'CN', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Guangzhou', + AvgTicketPrice: 184.57886708644122, + DistanceMiles: 746.8977963958768, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 1202.015487242926, + timestamp: '2018-01-01T23:53:52', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 109.27413520390236, + Origin: 'Guangzhou Baiyun International Airport', + OriginLocation: { lat: '23.39240074', lon: '113.2990036' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CAN', + OriginRegion: 'SE-BD', + DestCityName: 'Shanghai', + FlightTimeHour: 1.821235586731706, + FlightDelayMin: 0, + }, + { + FlightNum: 'NOFPUHM', + DestCountry: 'CH', + OriginWeather: 'Hail', + OriginCityName: 'Miami', + AvgTicketPrice: 650.3807641564442, + DistanceMiles: 4883.762668168842, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 7859.654147441516, + timestamp: '2018-01-01T16:25:52', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 714.5140134037742, + Origin: 'Miami International Airport', + OriginLocation: { lat: '25.79319954', lon: '-80.29060364' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'MIA', + OriginRegion: 'US-FL', + DestCityName: 'Zurich', + FlightTimeHour: 11.908566890062904, + FlightDelayMin: 0, + }, + { + FlightNum: 'S0SNYUV', + DestCountry: 'GB', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Dubai', + AvgTicketPrice: 505.3645318454166, + DistanceMiles: 3420.471874540241, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'London Heathrow Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 5504.71588846009, + timestamp: '2018-01-01T00:43:34', + DestLocation: { lat: '51.4706', lon: '-0.461941' }, + DestAirportID: 'LHR', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 485.8175493588939, + Origin: 'Dubai International Airport', + OriginLocation: { lat: '25.25279999', lon: '55.36439896' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'DXB', + OriginRegion: 'SE-BD', + DestCityName: 'London', + FlightTimeHour: 8.096959155981565, + FlightDelayMin: 180, + }, + { + FlightNum: 'FZ1FWP0', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Mexico City', + AvgTicketPrice: 937.7339299906702, + DistanceMiles: 2230.671063160962, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 3589.917091471716, + timestamp: '2018-01-01T20:42:03', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 211.17159361598328, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'CA-ON', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Ottawa', + FlightTimeHour: 3.519526560266388, + FlightDelayMin: 0, + }, + { + FlightNum: 'SUXIZJA', + DestCountry: 'GB', + OriginWeather: 'Hail', + OriginCityName: 'Osaka', + AvgTicketPrice: 531.5562550465518, + DistanceMiles: 5873.817368388099, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Manchester Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 9452.992738911176, + timestamp: '2018-01-01T14:00:01', + DestLocation: { lat: '53.35369873', lon: '-2.274950027' }, + DestAirportID: 'MAN', + Carrier: 'ES-Air', + Cancelled: true, + FlightTimeMin: 556.0583964065398, + Origin: 'Kansai International Airport', + OriginLocation: { lat: '34.4272995', lon: '135.2440033' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'KIX', + OriginRegion: 'SE-BD', + DestCityName: 'Manchester', + FlightTimeHour: 9.267639940108998, + FlightDelayMin: 0, + }, + { + FlightNum: '92JKFYR', + DestCountry: 'CL', + OriginWeather: 'Sunny', + OriginCityName: 'Tokoname', + AvgTicketPrice: 885.4770333086404, + DistanceMiles: 10864.104232263331, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Comodoro Arturo Merino Benitez International Airport', + FlightDelayType: 'Weather Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 17484.0809615676, + timestamp: '2018-01-01T14:00:01', + DestLocation: { lat: '-33.39300156', lon: '-70.78579712' }, + DestAirportID: 'SCL', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 1404.9293047359693, + Origin: 'Chubu Centrair International Airport', + OriginLocation: { lat: '34.85839844', lon: '136.8049927' }, + DestRegion: 'SE-BD', + OriginAirportID: 'NGO', + OriginRegion: 'SE-BD', + DestCityName: 'Santiago', + FlightTimeHour: 23.415488412266154, + FlightDelayMin: 60, + }, + { + FlightNum: 'WG1N4RX', + DestCountry: 'RU', + OriginWeather: 'Clear', + OriginCityName: 'Brisbane', + AvgTicketPrice: 305.3721105628916, + DistanceMiles: 8710.169059970298, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Olenya Air Base', + FlightDelayType: 'No Delay', + OriginCountry: 'AU', + dayOfWeek: 0, + DistanceKilometers: 14017.658315648841, + timestamp: '2018-01-01T06:32:21', + DestLocation: { lat: '68.15180206', lon: '33.46390152' }, + DestAirportID: 'XLMO', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 667.507538840421, + Origin: 'Brisbane International Airport', + OriginLocation: { lat: '-27.38419914', lon: '153.1170044' }, + DestRegion: 'RU-MUR', + OriginAirportID: 'BNE', + OriginRegion: 'SE-BD', + DestCityName: 'Olenegorsk', + FlightTimeHour: 11.12512564734035, + FlightDelayMin: 0, + }, + { + FlightNum: 'XKFZHUO', + DestCountry: 'RU', + OriginWeather: 'Sunny', + OriginCityName: 'Pisa', + AvgTicketPrice: 694.4927471612291, + DistanceMiles: 1461.43146405065, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'Sheremetyevo International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 2351.9459580811294, + timestamp: '2018-01-01T01:01:51', + DestLocation: { lat: '55.972599', lon: '37.4146' }, + DestAirportID: 'SVO', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 171.79639720540862, + Origin: 'Pisa International Airport', + OriginLocation: { lat: '43.683899', lon: '10.3927' }, + DestRegion: 'RU-MOS', + OriginAirportID: 'PI05', + OriginRegion: 'IT-52', + DestCityName: 'Moscow', + FlightTimeHour: 2.8632732867568103, + FlightDelayMin: 15, + }, + { + FlightNum: 'U4XV7HN', + DestCountry: 'IN', + OriginWeather: 'Clear', + OriginCityName: 'Cagliari', + AvgTicketPrice: 415.89453312482266, + DistanceMiles: 3888.394115635433, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Indira Gandhi International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6257.763739633191, + timestamp: '2018-01-01T10:44:44', + DestLocation: { lat: '28.5665', lon: '77.103104' }, + DestAirportID: 'DEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 568.8876126939265, + Origin: 'Cagliari Elmas Airport', + OriginLocation: { lat: '39.251499', lon: '9.05428' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CA07', + OriginRegion: 'IT-88', + DestCityName: 'New Delhi', + FlightTimeHour: 9.481460211565441, + FlightDelayMin: 0, + }, + { + FlightNum: 'R0JFGVC', + DestCountry: 'SE', + OriginWeather: 'Heavy Fog', + OriginCityName: 'London', + AvgTicketPrice: 551.0601339103603, + DistanceMiles: 888.5278930418095, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Stockholm-Arlanda Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 1429.947033499478, + timestamp: '2018-01-01T10:31:48', + DestLocation: { lat: '59.65190125', lon: '17.91860008' }, + DestAirportID: 'ARN', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 79.44150186108212, + Origin: 'London Luton Airport', + OriginLocation: { lat: '51.87469864', lon: '-0.368333012' }, + DestRegion: 'SE-AB', + OriginAirportID: 'LTN', + OriginRegion: 'GB-ENG', + DestCityName: 'Stockholm', + FlightTimeHour: 1.3240250310180353, + FlightDelayMin: 0, + }, + { + FlightNum: 'TF9BTQL', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Chitose / Tomakomai', + AvgTicketPrice: 798.7800550805481, + DistanceMiles: 5590.557988612903, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Rochester International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 8997.130955626244, + timestamp: '2018-01-01T06:45:49', + DestLocation: { lat: '43.90829849', lon: '-92.5' }, + DestAirportID: 'RST', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 499.8406086459024, + Origin: 'New Chitose Airport', + OriginLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestRegion: 'US-MN', + OriginAirportID: 'CTS', + OriginRegion: 'SE-BD', + DestCityName: 'Rochester', + FlightTimeHour: 8.33067681076504, + FlightDelayMin: 0, + }, + { + FlightNum: 'CKZBJJ8', + DestCountry: 'JP', + OriginWeather: 'Cloudy', + OriginCityName: 'Tokoname', + AvgTicketPrice: 117.01632685415828, + DistanceMiles: 606.1652997826461, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'New Chitose Airport', + FlightDelayType: 'Weather Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 975.5284882134029, + timestamp: '2018-01-01T12:13:01', + DestLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestAirportID: 'CTS', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 387.38402871843545, + Origin: 'Chubu Centrair International Airport', + OriginLocation: { lat: '34.85839844', lon: '136.8049927' }, + DestRegion: 'SE-BD', + OriginAirportID: 'NGO', + OriginRegion: 'SE-BD', + DestCityName: 'Chitose / Tomakomai', + FlightTimeHour: 6.456400478640591, + FlightDelayMin: 330, + }, + { + FlightNum: 'T9QK7GX', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Mumbai', + AvgTicketPrice: 841.2103842623986, + DistanceMiles: 9037.917857359074, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'San Antonio International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IN', + dayOfWeek: 0, + DistanceKilometers: 14545.118876233682, + timestamp: '2018-01-01T12:13:01', + DestLocation: { lat: '29.53370094', lon: '-98.46980286' }, + DestAirportID: 'SAT', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 909.0699297646051, + Origin: 'Chhatrapati Shivaji International Airport', + OriginLocation: { lat: '19.08869934', lon: '72.86789703' }, + DestRegion: 'US-TX', + OriginAirportID: 'BOM', + OriginRegion: 'SE-BD', + DestCityName: 'San Antonio', + FlightTimeHour: 15.151165496076752, + FlightDelayMin: 0, + }, + { + FlightNum: 'B5PRMI8', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Zurich', + AvgTicketPrice: 125.4083518148904, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T10:21:08', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: 'OE6XDRI', + DestCountry: 'AT', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Belogorsk', + AvgTicketPrice: 590.1173423147161, + DistanceMiles: 4498.903224225127, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'Vienna International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 7240.282910487363, + timestamp: '2018-01-01T23:10:15', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'ES-Air', + Cancelled: true, + FlightTimeMin: 614.7753766898744, + Origin: 'Ukrainka Air Base', + OriginLocation: { lat: '51.169997', lon: '128.445007' }, + DestRegion: 'AT-9', + OriginAirportID: 'XHBU', + OriginRegion: 'RU-AMU', + DestCityName: 'Vienna', + FlightTimeHour: 10.246256278164573, + FlightDelayMin: 270, + }, + { + FlightNum: '0VH5EM7', + DestCountry: 'IN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Mumbai', + AvgTicketPrice: 223.59354684926342, + DistanceMiles: 387.3167656891642, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Rajiv Gandhi International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IN', + dayOfWeek: 0, + DistanceKilometers: 623.3259129612622, + timestamp: '2018-01-01T20:53:18', + DestLocation: { lat: '17.23131752', lon: '78.42985535' }, + DestAirportID: 'HYD', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 34.62921738673679, + Origin: 'Chhatrapati Shivaji International Airport', + OriginLocation: { lat: '19.08869934', lon: '72.86789703' }, + DestRegion: 'SE-BD', + OriginAirportID: 'BOM', + OriginRegion: 'SE-BD', + DestCityName: 'Hyderabad', + FlightTimeHour: 0.5771536231122798, + FlightDelayMin: 0, + }, + { + FlightNum: 'NHLMKLG', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Zurich', + AvgTicketPrice: 178.41351273264587, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T02:59:15', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: '0LT8Q4U', + DestCountry: 'PL', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Beijing', + AvgTicketPrice: 416.9128422781053, + DistanceMiles: 4326.352348507669, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Warsaw Chopin Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 6962.589193956727, + timestamp: '2018-01-01T03:09:37', + DestLocation: { lat: '52.16569901', lon: '20.96710014' }, + DestAirportID: 'WAW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 580.2157661630606, + Origin: 'Beijing Capital International Airport', + OriginLocation: { lat: '40.08010101', lon: '116.5849991' }, + DestRegion: 'PL-MZ', + OriginAirportID: 'PEK', + OriginRegion: 'SE-BD', + DestCityName: 'Warsaw', + FlightTimeHour: 9.670262769384344, + FlightDelayMin: 0, + }, + { + FlightNum: 'E37OG3P', + DestCountry: 'GB', + OriginWeather: 'Hail', + OriginCityName: 'Cagliari', + AvgTicketPrice: 500.4828351053994, + DistanceMiles: 982.4372639008275, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'London Luton Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 1581.0795160352134, + timestamp: '2018-01-01T04:14:28', + DestLocation: { lat: '51.87469864', lon: '-0.368333012' }, + DestAirportID: 'LTN', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 112.93425114537239, + Origin: 'Cagliari Elmas Airport', + OriginLocation: { lat: '39.251499', lon: '9.05428' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'CA07', + OriginRegion: 'IT-88', + DestCityName: 'London', + FlightTimeHour: 1.8822375190895397, + FlightDelayMin: 0, + }, + { + FlightNum: 'VH5MDUD', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Frankfurt am Main', + AvgTicketPrice: 346.7483794066166, + DistanceMiles: 3953.1867605000593, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Lester B. Pearson International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 6362.037393890208, + timestamp: '2018-01-01T23:45:30', + DestLocation: { lat: '43.67720032', lon: '-79.63059998' }, + DestAirportID: 'YYZ', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 353.4465218827893, + Origin: 'Frankfurt am Main Airport', + OriginLocation: { lat: '50.033333', lon: '8.570556' }, + DestRegion: 'CA-ON', + OriginAirportID: 'FRA', + OriginRegion: 'DE-HE', + DestCityName: 'Toronto', + FlightTimeHour: 5.890775364713154, + FlightDelayMin: 0, + }, + { + FlightNum: 'M21BD4I', + DestCountry: 'CN', + OriginWeather: 'Cloudy', + OriginCityName: 'Tokyo', + AvgTicketPrice: 969.1899314970427, + DistanceMiles: 1754.4121462676542, + FlightDelay: true, + DestWeather: 'Clear', + Dest: "Xi'an Xianyang International Airport", + FlightDelayType: 'NAS Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 2823.4526611229717, + timestamp: '2018-01-01T20:17:26', + DestLocation: { lat: '34.447102', lon: '108.751999' }, + DestAirportID: 'XIY', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 261.1726330561486, + Origin: 'Tokyo Haneda International Airport', + OriginLocation: { lat: '35.552299', lon: '139.779999' }, + DestRegion: 'SE-BD', + OriginAirportID: 'HND', + OriginRegion: 'SE-BD', + DestCityName: "Xi'an", + FlightTimeHour: 4.352877217602477, + FlightDelayMin: 120, + }, + { + FlightNum: 'VDDH65N', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Chengdu', + AvgTicketPrice: 438.00156663741143, + DistanceMiles: 4923.026978969943, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Ciampino___G. B. Pastine International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 7922.843930443404, + timestamp: '2018-01-01T22:01:37', + DestLocation: { lat: '41.7994', lon: '12.5949' }, + DestAirportID: 'RM12', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 587.2782824020669, + Origin: 'Chengdu Shuangliu International Airport', + OriginLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestRegion: 'IT-62', + OriginAirportID: 'CTU', + OriginRegion: 'SE-BD', + DestCityName: 'Rome', + FlightTimeHour: 9.787971373367782, + FlightDelayMin: 210, + }, + { + FlightNum: 'QY97TB0', + DestCountry: 'IT', + OriginWeather: 'Clear', + OriginCityName: 'Milan', + AvgTicketPrice: 173.5006794979574, + DistanceMiles: 149.4599064359537, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 240.53240366326352, + timestamp: '2018-01-01T14:31:43', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 15.03327522895397, + Origin: 'Milano Linate Airport', + OriginLocation: { lat: '45.445099', lon: '9.27674' }, + DestRegion: 'IT-34', + OriginAirportID: 'MI11', + OriginRegion: 'IT-25', + DestCityName: 'Venice', + FlightTimeHour: 0.25055458714923284, + FlightDelayMin: 0, + }, +]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx b/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx new file mode 100644 index 000000000000..0aaa0e7a8a53 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Story } from '@storybook/react'; + +const bar = '#c5ced8'; +const panel = '#f7f9fa'; +const background = '#e0e6ec'; +const minHeight = 60; + +const panelStyle = { + height: 165, + width: 400, + background: panel, +}; + +const kqlBarStyle = { background: bar, padding: 16, minHeight, fontStyle: 'italic' }; + +const inputBarStyle = { background: '#fff', padding: 4, minHeight }; + +const layout = (OptionStory: Story) => ( + + KQL Bar + + + + + + + + + + + + + + + + + + +); + +export const decorators = [layout]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts b/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts new file mode 100644 index 000000000000..e405b704796e --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { map, uniq } from 'lodash'; +import { EuiSelectableOption } from '@elastic/eui'; + +import { flights } from '../../fixtures/flights'; + +export type Flight = typeof flights[number]; +export type FlightField = keyof Flight; + +export const getOptions = (field: string) => uniq(map(flights, field)).sort(); + +export const getEuiSelectableOptions = (field: string, search?: string): EuiSelectableOption[] => { + const options = getOptions(field) + .map((option) => ({ + label: option + '', + searchableLabel: option + '', + })) + .filter((option) => !search || option.label.toLowerCase().includes(search.toLowerCase())); + if (options.length > 10) options.length = 10; + return options; +}; + +export const flightFieldLabels: Record = { + AvgTicketPrice: 'Average Ticket Price', + Cancelled: 'Cancelled', + Carrier: 'Carrier', + dayOfWeek: 'Day of Week', + Dest: 'Destination', + DestAirportID: 'Destination Airport ID', + DestCityName: 'Destination City', + DestCountry: 'Destination Country', + DestLocation: 'Destination Location', + DestRegion: 'Destination Region', + DestWeather: 'Destination Weather', + DistanceKilometers: 'Distance (km)', + DistanceMiles: 'Distance (mi)', + FlightDelay: 'Flight Delay', + FlightDelayMin: 'Flight Delay (min)', + FlightDelayType: 'Flight Delay Type', + FlightNum: 'Flight Number', + FlightTimeHour: 'Flight Time (hr)', + FlightTimeMin: 'Flight Time (min)', + Origin: 'Origin', + OriginAirportID: 'Origin Airport ID', + OriginCityName: 'Origin City', + OriginCountry: 'Origin Country', + OriginLocation: 'Origin Location', + OriginRegion: 'Origin Region', + OriginWeather: 'Origin Weather', + timestamp: 'Timestamp', +}; + +export const flightFields = Object.keys(flightFieldLabels) as FlightField[]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx b/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx new file mode 100644 index 000000000000..d1ad3af0daf4 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useMemo, useState } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { decorators } from './decorators'; +import { getEuiSelectableOptions, flightFields, flightFieldLabels, FlightField } from './flights'; +import { OptionsListEmbeddableFactory, OptionsListEmbeddable } from '../control_types/options_list'; +import { ControlFrame } from '../control_frame/control_frame'; + +export default { + title: 'Input Controls', + description: '', + decorators, +}; + +interface OptionsListStorybookArgs { + fields: string[]; + twoLine: boolean; +} + +const storybookArgs = { + twoLine: false, + fields: ['OriginCityName', 'OriginWeather', 'DestCityName', 'DestWeather'], +}; + +const storybookArgTypes = { + fields: { + twoLine: { + control: { type: 'bool' }, + }, + control: { + type: 'check', + options: flightFields, + }, + }, +}; + +const OptionsListStoryComponent = ({ fields, twoLine }: OptionsListStorybookArgs) => { + const [embeddables, setEmbeddables] = useState([]); + + const optionsListEmbeddableFactory = useMemo( + () => + new OptionsListEmbeddableFactory( + ({ field, search }) => + new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)) + ), + [] + ); + + useEffect(() => { + const embeddableCreatePromises = fields.map((field) => { + return optionsListEmbeddableFactory.create({ + field, + id: '', + indexPattern: '', + multiSelect: true, + twoLineLayout: twoLine, + title: flightFieldLabels[field as FlightField], + }); + }); + Promise.all(embeddableCreatePromises).then((newEmbeddables) => setEmbeddables(newEmbeddables)); + }, [fields, optionsListEmbeddableFactory, twoLine]); + + return ( + + {embeddables.map((embeddable) => ( + + + + ))} + + ); +}; + +export const OptionsListStory = ({ fields, twoLine }: OptionsListStorybookArgs) => ( + +); + +OptionsListStory.args = storybookArgs; +OptionsListStory.argTypes = storybookArgTypes; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss new file mode 100644 index 000000000000..ad054be022c3 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss @@ -0,0 +1,14 @@ +.controlFrame--formControlLayout { + width: 100%; + min-width: $euiSize * 12.5; +} + +.controlFrame--control { + &.optionsList--filterBtnSingle { + height: 100%; + } +} + +.optionsList--filterBtnTwoLine { + width: 100%; +} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx new file mode 100644 index 000000000000..7fa8688ffb36 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.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 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 React, { useMemo } from 'react'; +import useMount from 'react-use/lib/useMount'; +import classNames from 'classnames'; +import { EuiFormControlLayout, EuiFormLabel, EuiFormRow } from '@elastic/eui'; + +import { InputControlEmbeddable } from '../embeddable/types'; + +import './control_frame.scss'; + +interface ControlFrameProps { + embeddable: InputControlEmbeddable; + twoLine?: boolean; +} + +export const ControlFrame = ({ twoLine, embeddable }: ControlFrameProps) => { + const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []); + + useMount(() => { + if (embeddableRoot.current && embeddable) embeddable.render(embeddableRoot.current); + }); + + const form = ( + {embeddable.getInput().title} + ) + } + > +

+ + ); + + return twoLine ? ( + + {form} + + ) : ( + form + ); +}; diff --git a/packages/kbn-common-utils/jest.config.js b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts similarity index 70% rename from packages/kbn-common-utils/jest.config.js rename to src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts index 08f1995c4742..63275f12076f 100644 --- a/packages/kbn-common-utils/jest.config.js +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-common-utils'], -}; +export { OptionsListEmbeddableFactory } from './options_list_embeddable_factory'; +export { OptionsListEmbeddable } from './options_list_embeddable'; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss new file mode 100644 index 000000000000..e9a4ef215733 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss @@ -0,0 +1,46 @@ +.optionsList--anchorOverride { + display:block; +} + +.optionsList--popoverOverride { + width: 100%; + height: 100%; +} + +.optionsList--items { + @include euiScrollBar; + + overflow-y: auto; + max-height: $euiSize * 30; + width: $euiSize * 25; + max-width: 100%; +} + +.optionsList--filterBtn { + .euiFilterButton__text-hasNotification { + flex-grow: 1; + justify-content: space-between; + width: 0; + } + &.optionsList--filterBtnSingle { + width: 100%; + } + &.optionsList--filterBtnPlaceholder { + .euiFilterButton__textShift { + color: $euiTextSubduedColor; + } + } +} + +.optionsList--filterGroupSingle { + box-shadow: none; + height: 100%; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: $euiBorderRadius - 1px; + border-bottom-right-radius: $euiBorderRadius - 1px; +} + +.optionsList--filterGroup { + width: 100%; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx new file mode 100644 index 000000000000..2feb527ff916 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx @@ -0,0 +1,173 @@ +/* + * 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 React, { useMemo, useEffect, useState, useCallback, useRef } from 'react'; +import { debounceTime, tap } from 'rxjs/operators'; +import useMount from 'react-use/lib/useMount'; +import classNames from 'classnames'; +import { Subject } from 'rxjs'; +import { EuiFilterButton, EuiFilterGroup, EuiPopover, EuiSelectableOption } from '@elastic/eui'; + +import { + OptionsListDataFetcher, + OptionsListEmbeddable, + OptionsListEmbeddableInput, +} from './options_list_embeddable'; +import { OptionsListStrings } from './options_list_strings'; +import { InputControlOutput } from '../../embeddable/types'; +import { OptionsListPopover } from './options_list_popover_component'; +import { withEmbeddableSubscription } from '../../../../../../embeddable/public'; + +import './options_list.scss'; + +const toggleAvailableOptions = ( + indices: number[], + availableOptions: EuiSelectableOption[], + enabled: boolean +) => { + const newAvailableOptions = [...availableOptions]; + indices.forEach((index) => (newAvailableOptions[index].checked = enabled ? 'on' : undefined)); + return newAvailableOptions; +}; + +interface OptionsListProps { + input: OptionsListEmbeddableInput; + fetchData: OptionsListDataFetcher; +} + +export const OptionsListInner = ({ input, fetchData }: OptionsListProps) => { + const [availableOptions, setAvailableOptions] = useState([]); + const selectedOptions = useRef>(new Set()); + + // raw search string is stored here so it is remembered when popover is closed. + const [searchString, setSearchString] = useState(''); + const [debouncedSearchString, setDebouncedSearchString] = useState(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [loading, setIsLoading] = useState(false); + + const typeaheadSubject = useMemo(() => new Subject(), []); + + useMount(() => { + typeaheadSubject + .pipe( + tap((rawSearchText) => setSearchString(rawSearchText)), + debounceTime(100) + ) + .subscribe((search) => setDebouncedSearchString(search)); + // default selections can be applied here... + }); + + const { indexPattern, timeRange, filters, field, query } = input; + useEffect(() => { + let canceled = false; + setIsLoading(true); + fetchData({ + search: debouncedSearchString, + indexPattern, + timeRange, + filters, + field, + query, + }).then((newOptions) => { + if (canceled) return; + setIsLoading(false); + // We now have new 'availableOptions', we need to ensure the previously selected options are still selected. + const enabledIndices: number[] = []; + selectedOptions.current?.forEach((selectedOption) => { + const optionIndex = newOptions.findIndex( + (availableOption) => availableOption.label === selectedOption + ); + if (optionIndex >= 0) enabledIndices.push(optionIndex); + }); + newOptions = toggleAvailableOptions(enabledIndices, newOptions, true); + setAvailableOptions(newOptions); + }); + return () => { + canceled = true; + }; + }, [indexPattern, timeRange, filters, field, query, debouncedSearchString, fetchData]); + + const updateItem = useCallback( + (index: number) => { + const item = availableOptions?.[index]; + if (!item) return; + + const toggleOff = availableOptions[index].checked === 'on'; + + const newAvailableOptions = toggleAvailableOptions([index], availableOptions, !toggleOff); + setAvailableOptions(newAvailableOptions); + + if (toggleOff) { + selectedOptions.current.delete(item.label); + } else { + selectedOptions.current.add(item.label); + } + }, + [availableOptions] + ); + + const selectedOptionsString = Array.from(selectedOptions.current).join( + OptionsListStrings.summary.getSeparator() + ); + const selectedOptionsLength = Array.from(selectedOptions.current).length; + + const { twoLineLayout } = input; + + const button = ( + setIsPopoverOpen((openState) => !openState)} + isSelected={isPopoverOpen} + numFilters={availableOptions.length} + hasActiveFilters={selectedOptionsLength > 0} + numActiveFilters={selectedOptionsLength} + > + {!selectedOptionsLength ? OptionsListStrings.summary.getPlaceholder() : selectedOptionsString} + + ); + + return ( + + setIsPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="upLeft" + ownFocus + repositionOnScroll + > + + + + ); +}; + +export const OptionsListComponent = withEmbeddableSubscription< + OptionsListEmbeddableInput, + InputControlOutput, + OptionsListEmbeddable, + { fetchData: OptionsListDataFetcher } +>(OptionsListInner); diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx new file mode 100644 index 000000000000..4dcc4a75dc1f --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx @@ -0,0 +1,63 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { EuiSelectableOption } from '@elastic/eui'; + +import { OptionsListComponent } from './options_list_component'; +import { Embeddable } from '../../../../../../embeddable/public'; +import { InputControlInput, InputControlOutput } from '../../embeddable/types'; + +interface OptionsListDataFetchProps { + field: string; + search?: string; + indexPattern: string; + query?: InputControlInput['query']; + filters?: InputControlInput['filters']; + timeRange?: InputControlInput['timeRange']; +} + +export type OptionsListDataFetcher = ( + props: OptionsListDataFetchProps +) => Promise; + +export const OPTIONS_LIST_CONTROL = 'optionsListControl'; +export interface OptionsListEmbeddableInput extends InputControlInput { + field: string; + indexPattern: string; + multiSelect: boolean; +} +export class OptionsListEmbeddable extends Embeddable< + OptionsListEmbeddableInput, + InputControlOutput +> { + public readonly type = OPTIONS_LIST_CONTROL; + + private node?: HTMLElement; + private fetchData: OptionsListDataFetcher; + + constructor( + input: OptionsListEmbeddableInput, + output: InputControlOutput, + fetchData: OptionsListDataFetcher + ) { + super(input, output); + this.fetchData = fetchData; + } + + reload = () => {}; + + public render = (node: HTMLElement) => { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + }; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts new file mode 100644 index 000000000000..e1850e6715e3 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.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 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 { EmbeddableFactoryDefinition } from '../../../../../../embeddable/public'; +import { + OptionsListDataFetcher, + OptionsListEmbeddable, + OptionsListEmbeddableInput, + OPTIONS_LIST_CONTROL, +} from './options_list_embeddable'; + +export class OptionsListEmbeddableFactory implements EmbeddableFactoryDefinition { + public type = OPTIONS_LIST_CONTROL; + private fetchData: OptionsListDataFetcher; + + constructor(fetchData: OptionsListDataFetcher) { + this.fetchData = fetchData; + } + + public create(initialInput: OptionsListEmbeddableInput) { + return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData)); + } + + public isEditable = () => Promise.resolve(false); + + public getDisplayName = () => 'Options List Control'; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx new file mode 100644 index 000000000000..cd558b99f9aa --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx @@ -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 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 React from 'react'; +import { + EuiFieldSearch, + EuiFilterSelectItem, + EuiIcon, + EuiLoadingChart, + EuiPopoverTitle, + EuiSelectableOption, + EuiSpacer, +} from '@elastic/eui'; + +import { Subject } from 'rxjs'; +import { OptionsListStrings } from './options_list_strings'; + +interface OptionsListPopoverProps { + loading: boolean; + typeaheadSubject: Subject; + searchString: string; + updateItem: (index: number) => void; + availableOptions: EuiSelectableOption[]; +} + +export const OptionsListPopover = ({ + loading, + updateItem, + searchString, + typeaheadSubject, + availableOptions, +}: OptionsListPopoverProps) => { + return ( + <> + + { + typeaheadSubject.next(event.target.value); + }} + value={searchString} + /> + +
+ {!loading && + availableOptions && + availableOptions.map((item, index) => ( + updateItem(index)} + > + {item.label} + + ))} + {loading && ( +
+
+ + +

{OptionsListStrings.popover.getLoadingMessage()}

+
+
+ )} + + {!loading && (!availableOptions || availableOptions.length === 0) && ( +
+
+ + +

{OptionsListStrings.popover.getEmptyMessage()}

+
+
+ )} +
+ + ); +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts new file mode 100644 index 000000000000..2211ae14cb9b --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.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 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 { i18n } from '@kbn/i18n'; + +export const OptionsListStrings = { + summary: { + getSeparator: () => + i18n.translate('presentationUtil.inputControls.optionsList.summary.separator', { + defaultMessage: ', ', + }), + getPlaceholder: () => + i18n.translate('presentationUtil.inputControls.optionsList.summary.placeholder', { + defaultMessage: 'Select...', + }), + }, + popover: { + getLoadingMessage: () => + i18n.translate('presentationUtil.inputControls.optionsList.popover.loading', { + defaultMessage: 'Loading filters', + }), + getEmptyMessage: () => + i18n.translate('presentationUtil.inputControls.optionsList.popover.empty', { + defaultMessage: 'No filters found', + }), + }, +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts b/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts new file mode 100644 index 000000000000..00be17932ba1 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { Filter, Query, TimeRange } from '../../../../../data/public'; +import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../../../../../embeddable/public'; + +export type InputControlInput = EmbeddableInput & { + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; + twoLineLayout?: boolean; +}; + +export type InputControlOutput = EmbeddableOutput & { + filters?: Filter[]; +}; + +export type InputControlEmbeddable = IEmbeddable; diff --git a/packages/kbn-common-utils/src/index.ts b/src/plugins/presentation_util/public/components/input_controls/index.ts similarity index 93% rename from packages/kbn-common-utils/src/index.ts rename to src/plugins/presentation_util/public/components/input_controls/index.ts index 1b8bffe4bf15..5c2d5b68ae2e 100644 --- a/packages/kbn-common-utils/src/index.ts +++ b/src/plugins/presentation_util/public/components/input_controls/index.ts @@ -5,5 +5,3 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -export * from './json'; diff --git a/src/plugins/presentation_util/storybook/main.ts b/src/plugins/presentation_util/storybook/main.ts index 17b05404d0e4..09de9240c1ae 100644 --- a/src/plugins/presentation_util/storybook/main.ts +++ b/src/plugins/presentation_util/storybook/main.ts @@ -7,13 +7,12 @@ */ import { Configuration } from 'webpack'; -import { defaultConfig } from '@kbn/storybook'; -import webpackConfig from '@kbn/storybook/target/webpack.config'; +import { defaultConfig, WebpackConfig } from '@kbn/storybook'; module.exports = { ...defaultConfig, addons: ['@storybook/addon-essentials'], webpackFinal: (config: Configuration) => { - return webpackConfig({ config }); + return WebpackConfig({ config }); }, }; diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index b389d94b1941..caff10a90e84 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, @@ -13,6 +12,7 @@ "include": [ "common/**/*", "public/**/*", + "public/**/*.json", "server/**/*", "storybook/**/*", "../../../typings/**/*" @@ -21,5 +21,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../data/tsconfig.json" } ] } diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json index 899611d02746..fec191402f2a 100644 --- a/src/plugins/region_map/tsconfig.json +++ b/src/plugins/region_map/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index d9045b91b9df..b8761ab81fa7 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index 99849dea3861..0f26da69acd1 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index b0059c71424b..5a3f642a9d6e 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/screenshot_mode/tsconfig.json b/src/plugins/screenshot_mode/tsconfig.json index 58194b385448..832972ae0baa 100644 --- a/src/plugins/screenshot_mode/tsconfig.json +++ b/src/plugins/screenshot_mode/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index 530e01a034b0..6ebeff836f69 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index fea3e1b945f9..1662b1f4a2d4 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorDefinition } from '../locators'; import { UrlService, UrlServiceDependencies } from '../url_service'; -export interface TestLocatorState extends SerializableState { +export interface TestLocatorState extends SerializableRecord { savedObjectId: string; showFlyout: boolean; pageNumber: number; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index bae57b6d8a31..a251e87702c7 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import type { SavedObjectReference } from 'kibana/server'; import { DependencyList } from 'react'; -import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import type { PersistableState } from 'src/plugins/kibana_utils/common'; import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, @@ -30,7 +31,7 @@ export interface LocatorDependencies { getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } -export class Locator

implements LocatorPublic

{ +export class Locator

implements LocatorPublic

{ public readonly migrations: PersistableState

['migrations']; constructor( diff --git a/src/plugins/share/common/url_service/locators/locator_client.ts b/src/plugins/share/common/url_service/locators/locator_client.ts index fc6b23f94a38..587083551aa6 100644 --- a/src/plugins/share/common/url_service/locators/locator_client.ts +++ b/src/plugins/share/common/url_service/locators/locator_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { LocatorDependencies } from './locator'; import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types'; import { Locator } from './locator'; @@ -27,7 +27,7 @@ export class LocatorClient implements ILocatorClient { * @param definition A definition of URL locator. * @returns A public interface of URL locator. */ - public create

(definition: LocatorDefinition

): LocatorPublic

{ + public create

(definition: LocatorDefinition

): LocatorPublic

{ const locator = new Locator

(definition, this.deps); this.locators.set(definition.id, locator); @@ -41,7 +41,7 @@ export class LocatorClient implements ILocatorClient { * @param id ID of a URL locator. * @returns A public interface of a registered URL locator. */ - public get

(id: string): undefined | LocatorPublic

{ + public get

(id: string): undefined | LocatorPublic

{ return this.locators.get(id); } } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index 0429d52a8f52..ba9c6c9185d2 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { DependencyList } from 'react'; -import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { PersistableState } from 'src/plugins/kibana_utils/common'; /** * URL locator registry. @@ -18,20 +19,20 @@ export interface ILocatorClient { * * @param urlGenerator Definition of the new locator. */ - create

(locatorDefinition: LocatorDefinition

): LocatorPublic

; + create

(locatorDefinition: LocatorDefinition

): LocatorPublic

; /** * Retrieve a previously registered locator. * * @param id Unique ID of the locator. */ - get

(id: string): undefined | LocatorPublic

; + get

(id: string): undefined | LocatorPublic

; } /** * A convenience interface used to define and register a locator. */ -export interface LocatorDefinition

+export interface LocatorDefinition

extends Partial> { /** * Unique ID of the locator. Should be constant and unique across Kibana. @@ -50,7 +51,7 @@ export interface LocatorDefinition

/** * Public interface of a registered locator. */ -export interface LocatorPublic

extends PersistableState

{ +export interface LocatorPublic

extends PersistableState

{ /** * Returns a reference to a Kibana client-side location. * diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts index a84c712e1624..a8fefc5010bc 100644 --- a/src/plugins/share/common/url_service/locators/use_locator_url.ts +++ b/src/plugins/share/common/url_service/locators/use_locator_url.ts @@ -8,10 +8,10 @@ import { DependencyList, useEffect, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service'; -export const useLocatorUrl =

( +export const useLocatorUrl =

( locator: LocatorPublic

| null | undefined, params: P, getUrlParams?: LocatorGetUrlParams, diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts index 7d21ef5c8fca..3333878676e2 100644 --- a/src/plugins/share/public/mocks.ts +++ b/src/plugins/share/public/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; import { SharePluginSetup, SharePluginStart } from '.'; import { LocatorPublic, UrlService } from '../common/url_service'; @@ -42,7 +42,7 @@ const createStartContract = (): Start => { return startContract; }; -const createLocator = (): jest.Mocked< +const createLocator = (): jest.Mocked< LocatorPublic > => ({ getLocation: jest.fn(), diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts index ad99be43f678..494fb623a48a 100644 --- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -9,8 +9,8 @@ import type { CoreSetup } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; import { migrateToLatest } from '../../../../kibana_utils/common'; -import type { SerializableState } from '../../../../kibana_utils/common'; import type { UrlService } from '../../../common/url_service'; import { render } from './render'; import { parseSearchParams } from './util/parse_search_params'; @@ -23,7 +23,7 @@ export interface RedirectOptions { version: string; /** Locator params. */ - params: unknown & SerializableState; + params: unknown & SerializableRecord; } export interface RedirectManagerDependencies { diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts index a60c1d1b68a9..7745e6dad43b 100644 --- a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import type { RedirectOptions } from '../redirect_manager'; @@ -63,7 +63,7 @@ export function parseSearchParams(urlSearch: string): RedirectOptions { throw new Error(message); } - let params: unknown & SerializableState; + let params: unknown & SerializableRecord; try { params = JSON.parse(paramsJson); } catch { diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 985066915f1d..c6afc06bc61c 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index b1b6a16958db..7492142f0d79 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -100,7 +100,7 @@ export interface SpacesApiUiComponent { * that there is a conflict, and it includes a button that will redirect the user to object B when clicked. * * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call - * `SavedObjectsClient.resolve()` (A) and the `aliasTargetId` value in the response (B). For example... + * `SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example... * * A is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. * diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json index 0cc82d7e5d12..35942863c1f1 100644 --- a/src/plugins/spaces_oss/tsconfig.json +++ b/src/plugins/spaces_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 710e209537b8..d50ccd563fe5 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 132997986060..adfe3bba0796 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 2daee868ac20..0f00f12e71c2 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json index 899611d02746..fec191402f2a 100644 --- a/src/plugins/tile_map/tsconfig.json +++ b/src/plugins/tile_map/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json index 5b96d69a878e..594901c3cc1e 100644 --- a/src/plugins/timelion/tsconfig.json +++ b/src/plugins/timelion/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index f8c473a3e2c0..8e4e61d4cafc 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -13,6 +13,7 @@ import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PublicMethodsOf } from '@kbn/utility-types'; import React from 'react'; +import { SerializableRecord } from '@kbn/utility-types'; import { UiComponent } from 'src/plugins/kibana_utils/public'; // Warning: (ae-missing-release-tag) "Action" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json index a871d7215cdc..d53d60ec73cd 100644 --- a/src/plugins/ui_actions/tsconfig.json +++ b/src/plugins/ui_actions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index 8e867a6bad14..c6ef2a0286da 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 68a0853994e8..7fac30a8048e 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json index 34003bced5ad..3d8fb6778c85 100644 --- a/src/plugins/vis_default_editor/tsconfig.json +++ b/src/plugins/vis_default_editor/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json index d5ab89b98081..7c32f4493519 100644 --- a/src/plugins/vis_type_markdown/tsconfig.json +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json index bee666a5906c..e430ec246079 100644 --- a/src/plugins/vis_type_metric/tsconfig.json +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_pie/tsconfig.json b/src/plugins/vis_type_pie/tsconfig.json index 69bd2855b984..9640447b35d9 100644 --- a/src/plugins/vis_type_pie/tsconfig.json +++ b/src/plugins/vis_type_pie/tsconfig.json @@ -1,25 +1,24 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../charts/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - { "path": "../field_formats/tsconfig.json" } - ] - } \ No newline at end of file + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../field_formats/tsconfig.json" } + ] +} diff --git a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx index 458bca4a5406..01dd693a31ff 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx @@ -99,7 +99,7 @@ export const TableVisControls = memo( position="top" content={i18n.translate('visTypeTable.vis.controls.exportButtonFormulasWarning', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', })} > {button} diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index 50277d51e874..16f2f809bde3 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json index 18bbad225746..021237dd7ad5 100644 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_timelion/common/vis_data.ts b/src/plugins/vis_type_timelion/common/vis_data.ts index e3041f43a8f1..bab6198be603 100644 --- a/src/plugins/vis_type_timelion/common/vis_data.ts +++ b/src/plugins/vis_type_timelion/common/vis_data.ts @@ -30,4 +30,5 @@ export interface VisSeries { color?: string; data: Array>; stack: boolean; + _hide?: boolean; } diff --git a/src/plugins/vis_type_timelion/public/components/series/area.tsx b/src/plugins/vis_type_timelion/public/components/series/area.tsx index 589a488d3aca..73e16d97684d 100644 --- a/src/plugins/vis_type_timelion/public/components/series/area.tsx +++ b/src/plugins/vis_type_timelion/public/components/series/area.tsx @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +// @ts-ignore +import chroma from 'chroma-js'; import React from 'react'; import { AreaSeries, ScaleType, CurveType, AreaSeriesStyle, PointShape } from '@elastic/charts'; import type { VisSeries } from '../../../common/vis_data'; @@ -19,6 +21,16 @@ interface AreaSeriesComponentProps { const isShowLines = (lines: VisSeries['lines'], points: VisSeries['points']) => lines?.show ? true : points?.show ? false : true; +const getPointFillColor = (points: VisSeries['points'], color: string | undefined) => { + const pointFillColor = points?.fillColor || points?.fill === undefined ? 'white' : color; + return ( + pointFillColor && + chroma(pointFillColor) + .alpha(points?.fill ?? 1) + .css() + ); +}; + const getAreaSeriesStyle = ({ color, lines, points }: AreaSeriesComponentProps['visData']) => ({ line: { @@ -33,8 +45,8 @@ const getAreaSeriesStyle = ({ color, lines, points }: AreaSeriesComponentProps[' visible: lines?.show ?? points?.show ?? true, }, point: { - fill: points?.fillColor ?? color, - opacity: points?.lineWidth !== undefined ? (points.fill || 1) * 10 : 10, + fill: getPointFillColor(points, color), + opacity: 1, radius: points?.radius ?? 3, stroke: color, strokeWidth: points?.lineWidth ?? 2, @@ -53,7 +65,7 @@ export const AreaSeriesComponent = ({ index, groupId, visData }: AreaSeriesCompo yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]} - data={visData.data} + data={visData._hide ? [] : visData.data} sortIndex={index} color={visData.color} stackAccessors={visData.stack ? [0] : undefined} diff --git a/src/plugins/vis_type_timelion/public/components/series/bar.tsx b/src/plugins/vis_type_timelion/public/components/series/bar.tsx index 6a97c8fea969..0a26fb51c32c 100644 --- a/src/plugins/vis_type_timelion/public/components/series/bar.tsx +++ b/src/plugins/vis_type_timelion/public/components/series/bar.tsx @@ -27,14 +27,12 @@ const getBarSeriesStyle = ({ color, bars }: BarSeriesComponentProps['visData']) return { rectBorder: { - stroke: color, - strokeWidth: Math.max(1, bars.lineWidth ? Math.ceil(bars.lineWidth / 2) : 1), - visible: true, + visible: false, }, rect: { fill: color, opacity, - widthPixel: 1, + widthPixel: Math.max(1, bars.lineWidth ?? 1), }, } as BarSeriesStyle; }; @@ -48,7 +46,7 @@ export const BarSeriesComponent = ({ index, groupId, visData }: BarSeriesCompone yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]} - data={visData.data} + data={visData._hide ? [] : visData.data} sortIndex={index} enableHistogramMode={false} color={visData.color} diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json index 77f97de28366..efeab8d73db1 100644 --- a/src/plugins/vis_type_timelion/tsconfig.json +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index 66dbdaaabcdd..0a110dd65d5e 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { getTimezone } from './application/lib/get_timezone'; import { getUISettings, getDataStart, getCoreStart } from './services'; import { ROUTES } from '../common/constants'; @@ -19,7 +19,7 @@ interface MetricsRequestHandlerParams { uiState: Record; visParams: TimeseriesVisParams; searchSessionId?: string; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } export const metricsRequestHandler = async ({ diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json index 7b2dd4b608c1..68097d8cff78 100644 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.test.ts b/src/plugins/vis_type_vega/public/data_model/search_api.test.ts new file mode 100644 index 000000000000..d0739453e43e --- /dev/null +++ b/src/plugins/vis_type_vega/public/data_model/search_api.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { extendSearchParamsWithRuntimeFields } from './search_api'; +import { dataPluginMock } from '../../../data/public/mocks'; + +import { getSearchParamsFromRequest, DataPublicPluginStart } from '../../../data/public'; + +const mockComputedFields = ( + dataStart: DataPublicPluginStart, + index: string, + runtimeFields: Record +) => { + dataStart.indexPatterns.find = jest.fn().mockReturnValue([ + { + title: index, + getComputedFields: () => ({ + runtimeFields, + }), + }, + ]); +}; + +describe('extendSearchParamsWithRuntimeFields', () => { + let dataStart: DataPublicPluginStart; + + beforeEach(() => { + dataStart = dataPluginMock.createStartContract(); + }); + + test('should inject default runtime_mappings for known indexes', async () => { + const requestParams = {}; + const runtimeFields = { foo: {} }; + + mockComputedFields(dataStart, 'index', runtimeFields); + + expect( + await extendSearchParamsWithRuntimeFields(dataStart.indexPatterns, requestParams, 'index') + ).toMatchInlineSnapshot(` + Object { + "body": Object { + "runtime_mappings": Object { + "foo": Object {}, + }, + }, + } + `); + }); + + test('should use runtime mappings from spec if it is specified', async () => { + const requestParams = ({ + body: { + runtime_mappings: { + test: {}, + }, + }, + } as unknown) as ReturnType; + const runtimeFields = { foo: {} }; + + mockComputedFields(dataStart, 'index', runtimeFields); + + expect( + await extendSearchParamsWithRuntimeFields(dataStart.indexPatterns, requestParams, 'index') + ).toMatchInlineSnapshot(` + Object { + "body": Object { + "runtime_mappings": Object { + "test": Object {}, + }, + }, + } + `); + }); +}); diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index c6aba6eccdc9..7a468b7bfce1 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -8,8 +8,7 @@ import { combineLatest, from } from 'rxjs'; import { map, tap, switchMap } from 'rxjs/operators'; -import { CoreStart, IUiSettingsClient } from 'kibana/public'; -import { getData } from '../services'; +import type { CoreStart, IUiSettingsClient } from 'kibana/public'; import { getSearchParamsFromRequest, SearchRequest, @@ -17,22 +16,28 @@ import { IEsSearchResponse, } from '../../../data/public'; import { search as dataPluginSearch } from '../../../data/public'; -import { VegaInspectorAdapters } from '../vega_inspector'; -import { RequestResponder } from '../../../inspector/public'; +import type { VegaInspectorAdapters } from '../vega_inspector'; +import type { RequestResponder } from '../../../inspector/public'; -const extendSearchParamsWithRuntimeFields = async ( +/** @internal **/ +export const extendSearchParamsWithRuntimeFields = async ( + indexPatterns: SearchAPIDependencies['indexPatterns'], requestParams: ReturnType, indexPatternString?: string ) => { if (indexPatternString) { - const indexPattern = (await getData().indexPatterns.find(indexPatternString)).find( - (index) => index.title === indexPatternString - ); - const runtimeFields = indexPattern?.getComputedFields().runtimeFields; + let runtimeMappings = requestParams.body?.runtime_mappings; + + if (!runtimeMappings) { + const indexPattern = (await indexPatterns.find(indexPatternString)).find( + (index) => index.title === indexPatternString + ); + runtimeMappings = indexPattern?.getComputedFields().runtimeFields; + } return { ...requestParams, - body: { ...requestParams.body, runtime_mappings: runtimeFields }, + body: { ...requestParams.body, runtime_mappings: runtimeMappings }, }; } @@ -43,6 +48,7 @@ export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; injectedMetadata: CoreStart['injectedMetadata']; search: DataPublicPluginStart['search']; + indexPatterns: DataPublicPluginStart['indexPatterns']; } export class SearchAPI { @@ -54,7 +60,7 @@ export class SearchAPI { ) {} search(searchRequests: SearchRequest[]) { - const { search } = this.dependencies; + const { search, indexPatterns } = this.dependencies; const requestResponders: any = {}; return combineLatest( @@ -64,15 +70,19 @@ export class SearchAPI { getConfig: this.dependencies.uiSettings.get.bind(this.dependencies.uiSettings), }); - if (this.inspectorAdapters) { - requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, { - ...request, - searchSessionId: this.searchSessionId, - }); - requestResponders[requestId].json(requestParams.body); - } - - return from(extendSearchParamsWithRuntimeFields(requestParams, request.index)).pipe( + return from( + extendSearchParamsWithRuntimeFields(indexPatterns, requestParams, request.index) + ).pipe( + tap((params) => { + /** inspect request data **/ + if (this.inspectorAdapters) { + requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, { + ...request, + searchSessionId: this.searchSessionId, + }); + requestResponders[requestId].json(params.body); + } + }), switchMap((params) => search .search( diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index eba5f9a40c9d..4c523714a253 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -45,10 +45,13 @@ export function createVegaRequestHandler( searchSessionId, }: VegaRequestHandlerParams) { if (!searchAPI) { + const { search, indexPatterns } = getData(); + searchAPI = new SearchAPI( { uiSettings, - search: getData().search, + search, + indexPatterns, injectedMetadata: getInjectedMetadata(), }, context.abortSignal, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index ee3bf305e942..17a098649ebb 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -129,6 +129,7 @@ describe('vega_map_view/view', () => { JSON.stringify(vegaMap), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 776f8898b3e3..ba1121b8894e 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -92,6 +92,7 @@ describe('VegaVisualizations', () => { JSON.stringify(vegaliteGraph), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), @@ -123,6 +124,7 @@ describe('VegaVisualizations', () => { JSON.stringify(vegaGraph), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index 4091dafcbe35..e1b8b5d9d4ba 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json index 5bf1af9ba75f..0d2a4094f04b 100644 --- a/src/plugins/vis_type_vislib/tsconfig.json +++ b/src/plugins/vis_type_vislib/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_xy/tsconfig.json b/src/plugins/vis_type_xy/tsconfig.json index 5cb0bc8d0bc8..0e4e41c286bd 100644 --- a/src/plugins/vis_type_xy/tsconfig.json +++ b/src/plugins/vis_type_xy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index d7ea04daae1a..b71542a8beee 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -384,6 +384,15 @@ export class VisualizeEmbeddable }; private async updateHandler() { + const context = { + type: 'visualization', + name: this.vis.type.title, + id: this.vis.id ?? 'an_unsaved_vis', + description: this.vis.title || this.input.title || this.vis.type.name, + url: this.output.editUrl, + parent: this.parent?.getInput().executionContext, + }; + const expressionParams: IExpressionLoaderParams = { searchContext: { timeRange: this.timeRange, @@ -394,13 +403,7 @@ export class VisualizeEmbeddable syncColors: this.input.syncColors, uiState: this.vis.uiState, inspectorAdapters: this.inspectorAdapters, - executionContext: this.deps.start().core.executionContext.create({ - type: 'visualization', - name: this.vis.type.name, - id: this.vis.id ?? 'an_unsaved_vis', - description: this.vis.title ?? this.vis.type.title, - url: this.output.editUrl, - }), + executionContext: context, }; if (this.abortController) { this.abortController.abort(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index fd22489eb755..872132416352 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -59,10 +59,7 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick< - VisualizationsStartDeps, - 'inspector' | 'embeddable' | 'savedObjectsClient' | 'executionContext' - > + Pick >; } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 97800ef40e8a..901593626a94 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -64,7 +64,6 @@ const createInstance = async () => { getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, savedObjects: savedObjectsPluginMock.createStartContract(), - executionContext: coreMock.createStart().executionContext, }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index f5b2f0edd044..ae97080b31fc 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -54,7 +54,6 @@ import type { Plugin, ApplicationStart, SavedObjectsClientContract, - ExecutionContextServiceStart, } from '../../../core/public'; import type { UsageCollectionSetup } from '../../usage_collection/public'; import type { UiActionsStart } from '../../ui_actions/public'; @@ -104,7 +103,6 @@ export interface VisualizationsStartDeps { getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; - executionContext: ExecutionContextServiceStart; } /** diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts index 0f549584af67..043f6195cf7b 100644 --- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts +++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts @@ -8,7 +8,7 @@ import { flow } from 'lodash'; import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; -import { SerializableState } from '../../../kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, @@ -18,42 +18,42 @@ import { commonMigrateTagCloud, } from '../migrations/visualization_common_migrations'; -const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => { +const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableRecord) => { return { ...state, savedVis: commonAddSupportOfDualIndexSelectionModeInTSVB(state.savedVis), }; }; -const byValueHideTSVBLastValueIndicator = (state: SerializableState) => { +const byValueHideTSVBLastValueIndicator = (state: SerializableRecord) => { return { ...state, savedVis: commonHideTSVBLastValueIndicator(state.savedVis), }; }; -const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableState) => { +const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableRecord) => { return { ...state, savedVis: commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel(state.savedVis), }; }; -const byValueAddEmptyValueColorRule = (state: SerializableState) => { +const byValueAddEmptyValueColorRule = (state: SerializableRecord) => { return { ...state, savedVis: commonAddEmptyValueColorRule(state.savedVis), }; }; -const byValueMigrateVislibPie = (state: SerializableState) => { +const byValueMigrateVislibPie = (state: SerializableRecord) => { return { ...state, savedVis: commonMigrateVislibPie(state.savedVis), }; }; -const byValueMigrateTagcloud = (state: SerializableState) => { +const byValueMigrateTagcloud = (state: SerializableRecord) => { return { ...state, savedVis: commonMigrateTagCloud(state.savedVis), diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 356448aa5977..65ab83d5e0ba 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index bc0891f39174..4dcf43dadf8b 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/test/common/services/index.ts b/test/common/services/index.ts index 370d28909351..c04bd778468a 100644 --- a/test/common/services/index.ts +++ b/test/common/services/index.ts @@ -15,6 +15,7 @@ import { RandomnessService } from './randomness'; import { SecurityServiceProvider } from './security'; import { EsDeleteAllIndicesProvider } from './es_delete_all_indices'; import { SavedObjectInfoService } from './saved_object_info'; +import { IndexPatternsService } from './index_patterns'; export const services = { deployment: DeploymentService, @@ -26,4 +27,5 @@ export const services = { security: SecurityServiceProvider, esDeleteAllIndices: EsDeleteAllIndicesProvider, savedObjectInfo: SavedObjectInfoService, + indexPatterns: IndexPatternsService, }; diff --git a/test/common/services/index_patterns.ts b/test/common/services/index_patterns.ts new file mode 100644 index 000000000000..5b6d20990b6d --- /dev/null +++ b/test/common/services/index_patterns.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 { FtrService } from '../ftr_provider_context'; +import { IndexPatternSpec } from '../../../src/plugins/data/common'; + +export class IndexPatternsService extends FtrService { + private readonly kibanaServer = this.ctx.getService('kibanaServer'); + + /** + * Create a new index pattern + */ + async create( + indexPattern: { title: string }, + { override = false }: { override: boolean } = { override: false } + ): Promise { + const response = await this.kibanaServer.request<{ + index_pattern: IndexPatternSpec; + }>({ + path: '/api/index_patterns/index_pattern', + method: 'POST', + body: { + override, + index_pattern: indexPattern, + }, + }); + + return response.data.index_pattern; + } +} diff --git a/test/examples/config.js b/test/examples/config.js index d47748e5f22a..85068740af46 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -22,6 +22,7 @@ export default async function ({ readConfigFile }) { return { testFiles: [ + require.resolve('./hello_world'), require.resolve('./embeddables'), require.resolve('./bfetch_explorer'), require.resolve('./ui_actions'), diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 7d0228131cc6..09a162e051bf 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -228,13 +228,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should make the document table scrollable', async function () { await PageObjects.discover.clearFieldSearchInput(); - const dscTable = await find.byCssSelector('.dscTable'); + const dscTableWrapper = await find.byCssSelector('.kbnDocTableWrapper'); const fieldNames = await PageObjects.discover.getAllFieldNames(); - const clientHeight = await dscTable.getAttribute('clientHeight'); + const clientHeight = await dscTableWrapper.getAttribute('clientHeight'); let fieldCounter = 0; const checkScrollable = async () => { - const scrollWidth = await dscTable.getAttribute('scrollWidth'); - const clientWidth = await dscTable.getAttribute('clientWidth'); + const scrollWidth = await dscTableWrapper.getAttribute('scrollWidth'); + const clientWidth = await dscTableWrapper.getAttribute('clientWidth'); log.debug(`scrollWidth: ${scrollWidth}, clientWidth: ${clientWidth}`); return Number(scrollWidth) > Number(clientWidth); }; @@ -251,7 +251,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return await checkScrollable(); }); // so now we need to check if the horizontal scrollbar is displayed - const newClientHeight = await dscTable.getAttribute('clientHeight'); + const newClientHeight = await dscTableWrapper.getAttribute('clientHeight'); expect(Number(clientHeight)).to.be.above(Number(newClientHeight)); }); }); diff --git a/test/functional/apps/management/_field_formatter.js b/test/functional/apps/management/_field_formatter.js deleted file mode 100644 index 383b4faecc40..000000000000 --- a/test/functional/apps/management/_field_formatter.js +++ /dev/null @@ -1,53 +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 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 default function ({ getService, getPageObjects }) { - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['settings']); - const testSubjects = getService('testSubjects'); - - describe('field formatter', function () { - this.tags(['skipFirefox']); - - before(async function () { - await browser.setWindowSize(1200, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); - await kibanaServer.uiSettings.replace({}); - await kibanaServer.uiSettings.update({}); - }); - - after(async function afterAll() { - await PageObjects.settings.navigateTo(); - await esArchiver.emptyKibanaIndex(); - }); - - describe('set and change field formatter', function describeIndexTests() { - // addresses https://github.com/elastic/kibana/issues/93349 - it('can change format more than once', async function () { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaIndexPatterns(); - await PageObjects.settings.clickIndexPatternLogstash(); - await PageObjects.settings.clickAddField(); - await PageObjects.settings.setFieldType('Long'); - const formatRow = await testSubjects.find('formatRow'); - const formatRowToggle = ( - await formatRow.findAllByCssSelector('[data-test-subj="toggle"]') - )[0]; - - await formatRowToggle.click(); - await PageObjects.settings.setFieldFormat('duration'); - await PageObjects.settings.setFieldFormat('bytes'); - await PageObjects.settings.setFieldFormat('duration'); - await testSubjects.click('euiFlyoutCloseButton'); - await PageObjects.settings.closeIndexPatternFieldEditor(); - }); - }); - }); -} diff --git a/test/functional/apps/management/_field_formatter.ts b/test/functional/apps/management/_field_formatter.ts new file mode 100644 index 000000000000..60c1bbe7b3d1 --- /dev/null +++ b/test/functional/apps/management/_field_formatter.ts @@ -0,0 +1,571 @@ +/* + * 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 { ES_FIELD_TYPES } from '@kbn/field-types'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { FIELD_FORMAT_IDS } from '../../../../src/plugins/field_formats/common'; +import { WebElementWrapper } from '../../services/lib/web_element_wrapper'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['settings', 'common']); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + const indexPatterns = getService('indexPatterns'); + const toasts = getService('toasts'); + + describe('field formatter', function () { + this.tags(['skipFirefox']); + + before(async function () { + await browser.setWindowSize(1200, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.update({}); + }); + + after(async function afterAll() { + await PageObjects.settings.navigateTo(); + await esArchiver.emptyKibanaIndex(); + }); + + describe('set and change field formatter', function describeIndexTests() { + // addresses https://github.com/elastic/kibana/issues/93349 + it('can change format more than once', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.clickAddField(); + await PageObjects.settings.setFieldType('Long'); + const formatRow = await testSubjects.find('formatRow'); + const formatRowToggle = ( + await formatRow.findAllByCssSelector('[data-test-subj="toggle"]') + )[0]; + + await formatRowToggle.click(); + await PageObjects.settings.setFieldFormat('duration'); + await PageObjects.settings.setFieldFormat('bytes'); + await PageObjects.settings.setFieldFormat('duration'); + await testSubjects.click('euiFlyoutCloseButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); + }); + }); + + /** + * The purpose of these tests is to cover **editing experience** of different field formats editors, + * The logic of each converter is extensively covered by unit tests. + * TODO: these tests also could check field formats behaviour with different combinations of browser locale, timezone and ui settings + */ + describe('field format editors', () => { + describe('String format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.TEXT, + fieldValue: 'A regular text', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A regular text', + + // check available formats for ES_FIELD_TYPES.TEXT + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.TRUNCATE, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.TEXT, + fieldValue: 'A regular text', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'a regular text', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'lower'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'a keyword', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A KEYWORD', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'upper'); + }, + // check available formats for ES_FIELD_TYPES.KEYWORD + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.TRUNCATE, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'a keyword', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A Keyword', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'title'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'com.organizations.project.ClassName', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'c.o.p.ClassName', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'short'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'SGVsbG8gd29ybGQ=', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'Hello world', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'base64'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: '%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: '안녕 키바나', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'urlparam'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: '123456789', + applyFormatterType: FIELD_FORMAT_IDS.TRUNCATE, + expectFormattedValue: '123...', + beforeSave: async () => { + await testSubjects.setValue('truncateEditorLength', '3'); + }, + }, + { + fieldType: ES_FIELD_TYPES.INTEGER, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: '324', + // check available formats for ES_FIELD_TYPES.INTEGER + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + ]); + }); + + describe('Number format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.NUMBER, + expectFormattedValue: '324', + // check available formats for ES_FIELD_TYPES.LONG + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.NUMBER, + expectFormattedValue: '+324', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '+0,0'); + }, + }, + ]); + }); + + describe('URL format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 100, + applyFormatterType: FIELD_FORMAT_IDS.URL, + expectFormattedValue: 'https://elastic.co/?value=100', + beforeSave: async () => { + await testSubjects.setValue( + 'urlEditorUrlTemplate', + 'https://elastic.co/?value={{value}}' + ); + }, + expect: async (renderedValueContainer) => { + expect( + await (await renderedValueContainer.findByTagName('a')).getAttribute('href') + ).to.be('https://elastic.co/?value=100'); + }, + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 100, + applyFormatterType: FIELD_FORMAT_IDS.URL, + expectFormattedValue: 'url label', + beforeSave: async () => { + await testSubjects.setValue( + 'urlEditorUrlTemplate', + 'https://elastic.co/?value={{value}}' + ); + await testSubjects.setValue('urlEditorLabelTemplate', 'url label'); + }, + expect: async (renderedValueContainer) => { + expect( + await (await renderedValueContainer.findByTagName('a')).getAttribute('href') + ).to.be('https://elastic.co/?value=100'); + }, + }, + ]); + }); + + describe('Date format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.DATE, + fieldValue: '2021-08-05T15:05:37.151Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE, + expectFormattedValue: 'Aug 5, 2021', + beforeSave: async () => { + await testSubjects.setValue('dateEditorPattern', 'MMM D, YYYY'); + }, + // check available formats for ES_FIELD_TYPES.DATE + expectFormatterTypes: [ + FIELD_FORMAT_IDS.DATE, + FIELD_FORMAT_IDS.DATE_NANOS, + FIELD_FORMAT_IDS.RELATIVE_DATE, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.DATE_NANOS, + fieldValue: '2015-01-01T12:10:30.123456789Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE, + expectFormattedValue: 'Jan 1, 2015 @ 12:10:30.123', + // check available formats for ES_FIELD_TYPES.DATE_NANOS + expectFormatterTypes: [ + FIELD_FORMAT_IDS.DATE, + FIELD_FORMAT_IDS.DATE_NANOS, + FIELD_FORMAT_IDS.RELATIVE_DATE, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.DATE_NANOS, + fieldValue: '2015-01-01T12:10:30.123456789Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE_NANOS, + expectFormattedValue: 'Jan 1, 2015 @ 12:10:30.123456789', + }, + ]); + }); + + describe('Static lookup format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'look me up', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'looked up!', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'look me up'); + await testSubjects.setValue('~staticLookupEditorValue', 'looked up!'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'true', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + // check available formats for ES_FIELD_TYPES.BOOLEAN + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + expectFormattedValue: 'yes', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + await testSubjects.setValue('staticLookupEditorUnknownValue', 'no'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'false', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'no', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + await testSubjects.setValue('staticLookupEditorUnknownValue', 'no'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'false', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'false', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + }, + }, + ]); + }); + + describe('Other formats', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 123292, + applyFormatterType: FIELD_FORMAT_IDS.DURATION, + expectFormattedValue: '2 minutes', + beforeSave: async () => { + await testSubjects.setValue('durationEditorInputFormat', 'milliseconds'); + }, + }, + { + fieldType: ES_FIELD_TYPES.DOUBLE, + fieldValue: 0.1, + applyFormatterType: FIELD_FORMAT_IDS.PERCENT, + // check available formats for ES_FIELD_TYPES.DOUBLE + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + expectFormattedValue: '10.0%', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '0.0%'); + }, + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 1990000000, + applyFormatterType: FIELD_FORMAT_IDS.BYTES, + expectFormattedValue: '2GB', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '0b'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'red', + applyFormatterType: FIELD_FORMAT_IDS.COLOR, + expectFormattedValue: 'red', + beforeSave: async () => { + await testSubjects.click('colorEditorAddColor'); + await testSubjects.setValue('~colorEditorKeyPattern', 'red'); + await testSubjects.setValue('~colorEditorColorPicker', '#ffffff'); + await testSubjects.setValue('~colorEditorBackgroundPicker', '#ff0000'); + }, + expect: async (renderedValueContainer) => { + const span = await renderedValueContainer.findByTagName('span'); + expect(await span.getComputedStyle('color')).to.be('rgba(255, 255, 255, 1)'); + expect(await span.getComputedStyle('background-color')).to.be('rgba(255, 0, 0, 1)'); + }, + }, + ]); + }); + }); + }); + + /** + * Runs a field format editors tests covering data setup, editing a field and checking a resulting formatting in Discover app + * TODO: might be useful to reuse this util for runtime fields formats tests + * @param specs - {@link FieldFormatEditorSpecDescriptor} + */ + function testFormatEditors(specs: FieldFormatEditorSpecDescriptor[]) { + const indexTitle = 'field_formats_management_functional_tests'; + let indexPatternId: string; + let testDocumentId: string; + + before(async () => { + if ((await es.indices.exists({ index: indexTitle })).body) { + await es.indices.delete({ index: indexTitle }); + } + + await es.indices.create({ + index: indexTitle, + body: { + mappings: { + properties: specs.reduce((properties, spec, index) => { + properties[`${index}`] = { type: spec.fieldType }; + return properties; + }, {} as Record), + }, + }, + }); + + const docResult = await es.index({ + index: indexTitle, + body: specs.reduce((properties, spec, index) => { + properties[`${index}`] = spec.fieldValue; + return properties; + }, {} as Record), + refresh: 'wait_for', + }); + testDocumentId = docResult.body._id; + + const indexPatternResult = await indexPatterns.create( + { title: indexTitle }, + { override: true } + ); + indexPatternId = indexPatternResult.id!; + }); + + describe('edit formats', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternByName(indexTitle); + }); + + afterEach(async () => { + try { + await PageObjects.settings.controlChangeSave(); + } catch (e) { + // in case previous test failed in a state when save is disabled + await PageObjects.settings.controlChangeCancel(); + } + + await toasts.dismissAllToasts(); // dismiss "saved" toast, otherwise it could overlap save button for a next test + }); + + specs.forEach((spec, index) => { + const fieldName = `${index}`; + it( + `edit field format of "${fieldName}" field to "${spec.applyFormatterType}"` + + spec.expectFormatterTypes + ? `, and check available formats types` + : '', + async () => { + await PageObjects.settings.filterField(fieldName); + await PageObjects.settings.openControlsByName(fieldName); + await PageObjects.settings.toggleRow('formatRow'); + + if (spec.expectFormatterTypes) { + expect( + ( + await Promise.all( + ( + await (await testSubjects.find('editorSelectedFormatId')).findAllByTagName( + 'option' + ) + ).map((option) => option.getAttribute('value')) + ) + ).filter(Boolean) + ).to.eql(spec.expectFormatterTypes); + } + + await PageObjects.settings.setFieldFormat(spec.applyFormatterType); + if (spec.beforeSave) { + await spec.beforeSave(await testSubjects.find('formatRow')); + } + } + ); + }); + }); + + describe('check formats', async () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { + hash: `/doc/${indexPatternId}/${indexTitle}?id=${testDocumentId}`, + }); + await testSubjects.exists('doc-hit'); + }); + + specs.forEach((spec, index) => { + it(`check field format of "${index}" field`, async () => { + const renderedValue = await testSubjects.find(`tableDocViewRow-${index}-value`); + const text = await renderedValue.getVisibleText(); + expect(text).to.be(spec.expectFormattedValue); + if (spec.expect) { + await spec.expect(renderedValue); + } + }); + }); + }); + } +} + +/** + * Describes a field format editor test + */ +interface FieldFormatEditorSpecDescriptor { + /** + * Raw field value to put into document + */ + fieldValue: string | number | boolean | null; + /** + * Explicitly specify a type for a {@link fieldValue} + */ + fieldType: ES_FIELD_TYPES; + /** + * Type of a field formatter to apply + */ + applyFormatterType: FIELD_FORMAT_IDS; + + /** + * Optionally check available formats for {@link fieldType} + */ + expectFormatterTypes?: FIELD_FORMAT_IDS[]; + + /** + * Function to execute before field format is applied. + * Use it set specific configuration params for applied field formatter + * @param formatRowContainer - field format editor container + */ + beforeSave?: (formatRowContainer: WebElementWrapper) => Promise; + + /** + * An expected formatted value rendered by Discover app, + * Use this for final assertion + */ + expectFormattedValue: string; + + /** + * Run additional assertions on rendered element + */ + expect?: (renderedValueContainer: WebElementWrapper) => Promise; +} diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 8a2a66c41d42..49d56d6f4378 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -202,13 +202,7 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { - basePath = '', - shouldLoginIfPrompted = true, - hash = '', - search = '', - insertTimestamp = true, - } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -217,13 +211,11 @@ export class CommonPageObject extends FtrService { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}${appConfig.pathname}`, hash: hash || appConfig.hash, - search, }); } else { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}/app/${appName}`, hash, - search, }); } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index ecba9549cea0..ea727069c927 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -228,9 +228,13 @@ export class DashboardPageObject extends FtrService { */ public async expectToolbarPaginationDisplayed() { - const isLegacyDefault = this.discover.useLegacyTable(); + const isLegacyDefault = await this.discover.useLegacyTable(); if (isLegacyDefault) { - const subjects = ['btnPrevPage', 'btnNextPage', 'toolBarPagerText']; + const subjects = [ + 'pagination-button-previous', + 'pagination-button-next', + 'toolBarTotalDocsText', + ]; await Promise.all(subjects.map(async (subj) => await this.testSubjects.existOrFail(subj))); } else { const subjects = ['pagination-button-previous', 'pagination-button-next']; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json index 3d9d8ca9451d..86170d356140 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,10 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_utils/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/app_link_test/tsconfig.json b/test/plugin_functional/plugins/app_link_test/tsconfig.json index f77a5eaffc30..b53fafd70cf5 100644 --- a/test/plugin_functional/plugins/app_link_test/tsconfig.json +++ b/test/plugin_functional/plugins/app_link_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,6 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" } ] } diff --git a/test/plugin_functional/plugins/core_app_status/tsconfig.json b/test/plugin_functional/plugins/core_app_status/tsconfig.json index 0f0e7caf2b48..f380bcc8e8b3 100644 --- a/test/plugin_functional/plugins/core_app_status/tsconfig.json +++ b/test/plugin_functional/plugins/core_app_status/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, - "outDir": "./target", + "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, "declarationMap": true diff --git a/test/plugin_functional/plugins/core_history_block/tsconfig.json b/test/plugin_functional/plugins/core_history_block/tsconfig.json index ffd2cd261ab2..a6882ecb3d1e 100644 --- a/test/plugin_functional/plugins/core_history_block/tsconfig.json +++ b/test/plugin_functional/plugins/core_history_block/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"], "exclude": [], diff --git a/test/plugin_functional/plugins/core_http/tsconfig.json b/test/plugin_functional/plugins/core_http/tsconfig.json index 3d9d8ca9451d..eab76d901e42 100644 --- a/test/plugin_functional/plugins/core_http/tsconfig.json +++ b/test/plugin_functional/plugins/core_http/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json index 3d9d8ca9451d..eab76d901e42 100644 --- a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json index f77a5eaffc30..87e51c3eab37 100644 --- a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json index 3d9d8ca9451d..78476ce6697e 100644 --- a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../core_plugin_a/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json index f77a5eaffc30..010574f0c3be 100644 --- a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,6 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json index f77a5eaffc30..87e51c3eab37 100644 --- a/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json index 3d9d8ca9451d..eab76d901e42 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json index 21662b2b64a1..4c00a35a3db7 100644 --- a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json index f9b0443e0a8b..d346449e67c4 100644 --- a/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json index 72793a327d97..b0e6d4f5d84c 100644 --- a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json index f9b0443e0a8b..d346449e67c4 100644 --- a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json index d0d1f2d99295..d18f6a63263b 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, - "outDir": "./target", + "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, "declarationMap": true diff --git a/test/plugin_functional/plugins/data_search/tsconfig.json b/test/plugin_functional/plugins/data_search/tsconfig.json index 72793a327d97..e82f3fca8fb3 100644 --- a/test/plugin_functional/plugins/data_search/tsconfig.json +++ b/test/plugin_functional/plugins/data_search/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", @@ -10,6 +9,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json index f9b0443e0a8b..45babe322896 100644 --- a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/discover/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json index 72793a327d97..b0e6d4f5d84c 100644 --- a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json index 9d11a9850f15..9ac0d7726c37 100644 --- a/test/plugin_functional/plugins/index_patterns/tsconfig.json +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json index f77a5eaffc30..043ace6ce064 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,9 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json index 3d9d8ca9451d..adf3815905d1 100644 --- a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/navigation/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index e92dc717ae25..8cbb8696409b 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -1,14 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "types": [ - "node", - "jest", - "react", - "@emotion/react/types/css-prop" - ] + "outDir": "./target/types" }, "include": [ "index.ts", @@ -18,6 +11,9 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../../src/plugins/expressions/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json index f77a5eaffc30..8222b8f01100 100644 --- a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/management/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json index 3d9d8ca9451d..eab76d901e42 100644 --- a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json index da457c9ba32f..f148b232e21f 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json +++ b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json b/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json index 3d9d8ca9451d..eab76d901e42 100644 --- a/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json +++ b/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json index da457c9ba32f..f148b232e21f 100644 --- a/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/session_notifications/tsconfig.json b/test/plugin_functional/plugins/session_notifications/tsconfig.json index 3d9d8ca9451d..c50f2e3f119c 100644 --- a/test/plugin_functional/plugins/session_notifications/tsconfig.json +++ b/test/plugin_functional/plugins/session_notifications/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,8 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/telemetry/tsconfig.json b/test/plugin_functional/plugins/telemetry/tsconfig.json index 947eb49a61b3..b32ac67279f4 100644 --- a/test/plugin_functional/plugins/telemetry/tsconfig.json +++ b/test/plugin_functional/plugins/telemetry/tsconfig.json @@ -1,10 +1,12 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": ["public/**/*.ts", "types.ts", "../../../../typings/**/*"], "exclude": [], - "references": [{ "path": "../../../../src/core/tsconfig.json" }] + "references": [ + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/telemetry/tsconfig.json" }, + ] } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 05b1c2c11da0..6551c1496164 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types", }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/usage_collection/tsconfig.json b/test/plugin_functional/plugins/usage_collection/tsconfig.json index 3d9d8ca9451d..d6f7d08f1858 100644 --- a/test/plugin_functional/plugins/usage_collection/tsconfig.json +++ b/test/plugin_functional/plugins/usage_collection/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/usage_collection/tsconfig.json" }, ] } diff --git a/test/plugin_functional/test_suites/core_plugins/execution_context.ts b/test/plugin_functional/test_suites/core_plugins/execution_context.ts index 93dc494ba5d6..7dc9922dca51 100644 --- a/test/plugin_functional/test_suites/core_plugins/execution_context.ts +++ b/test/plugin_functional/test_suites/core_plugins/execution_context.ts @@ -25,13 +25,13 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await browser.execute(async () => { const coreStart = window._coreProvider.start.core; - const context = coreStart.executionContext.create({ + const context = { type: 'visualization', name: 'execution_context_app', // add a non-ASCII symbols to make sure it doesn't break the context propagation mechanism id: 'Visualization☺漢字', description: 'какое-то странное описание', - }); + }; const result = await coreStart.http.get('/execution_context/pass', { context, diff --git a/test/scripts/checks/type_check_plugin_public_api_docs.sh b/test/scripts/checks/type_check_plugin_public_api_docs.sh index 8acacb696c57..77fa76038f7c 100755 --- a/test/scripts/checks/type_check_plugin_public_api_docs.sh +++ b/test/scripts/checks/type_check_plugin_public_api_docs.sh @@ -4,7 +4,6 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Build TS Refs" \ node scripts/build_ts_refs \ - --ignore-type-failures \ --clean \ --no-cache \ --force diff --git a/test/scripts/jenkins_apm_cypress.sh b/test/scripts/jenkins_apm_cypress.sh new file mode 100755 index 000000000000..a1d2ab73b555 --- /dev/null +++ b/test/scripts/jenkins_apm_cypress.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_xpack.sh + +echo " -> Running APM cypress tests" +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "APM Cypress Tests" \ + node plugins/apm/scripts/ftr_e2e/cypress_run + +echo "" +echo "" diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json index 5069db62589c..14ebb2e7d00c 100644 --- a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "composite": true + "outDir": "./target/types", }, "include": [ "index.ts", diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json index 224aa42ef68d..6b27e9b4b7a6 100644 --- a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "composite": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/tsconfig.json b/test/tsconfig.json index dccbe8d715c5..c94d4445dd24 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/tsconfig.base.json b/tsconfig.base.json index 0c8fec7c88cd..9de81f68110c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,6 +21,8 @@ "jsx": "react", // Enables all strict type checking options. "strict": true, + // All TS projects should be composite and only include the files they select, and ref the files outside of the project + "composite": true, // save information about the project graph on disk "incremental": true, // Do not check d.ts files by default diff --git a/tsconfig.bazel.json b/tsconfig.bazel.json new file mode 100644 index 000000000000..892c727ef588 --- /dev/null +++ b/tsconfig.bazel.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "composite": false + } +} diff --git a/tsconfig.browser_bazel.json b/tsconfig.browser_bazel.json new file mode 100644 index 000000000000..3a950a756dd0 --- /dev/null +++ b/tsconfig.browser_bazel.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.browser.json", + "compilerOptions": { + "incremental": false, + "composite": false + } +} diff --git a/tsconfig.json b/tsconfig.json index 3d6c29875c90..769dd5ecb6a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "incremental": false + "outDir": "target/root_types" }, "include": [ "kibana.d.ts", @@ -16,122 +16,7 @@ "exclude": [], "references": [ { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "./src/plugins/advanced_settings/tsconfig.json" }, - { "path": "./src/plugins/apm_oss/tsconfig.json" }, - { "path": "./src/plugins/bfetch/tsconfig.json" }, - { "path": "./src/plugins/charts/tsconfig.json" }, - { "path": "./src/plugins/console/tsconfig.json" }, - { "path": "./src/plugins/dashboard/tsconfig.json" }, - { "path": "./src/plugins/discover/tsconfig.json" }, - { "path": "./src/plugins/data/tsconfig.json" }, - { "path": "./src/plugins/dev_tools/tsconfig.json" }, - { "path": "./src/plugins/embeddable/tsconfig.json" }, - { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "./src/plugins/expressions/tsconfig.json" }, - { "path": "./src/plugins/home/tsconfig.json" }, - { "path": "./src/plugins/inspector/tsconfig.json" }, - { "path": "./src/plugins/interactive_setup/tsconfig.json" }, - { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "./src/plugins/kibana_overview/tsconfig.json" }, - { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/legacy_export/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json" }, - { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/navigation/tsconfig.json" }, - { "path": "./src/plugins/newsfeed/tsconfig.json" }, - { "path": "./src/plugins/region_map/tsconfig.json" }, - { "path": "./src/plugins/saved_objects/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "./src/plugins/security_oss/tsconfig.json" }, - { "path": "./src/plugins/share/tsconfig.json" }, - { "path": "./src/plugins/spaces_oss/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/tile_map/tsconfig.json" }, - { "path": "./src/plugins/timelion/tsconfig.json" }, - { "path": "./src/plugins/ui_actions/tsconfig.json" }, - { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, - { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, - { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, - { "path": "./src/plugins/vis_type_table/tsconfig.json" }, - { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, - { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, - { "path": "./src/plugins/vis_type_pie/tsconfig.json" }, - { "path": "./src/plugins/visualizations/tsconfig.json" }, - { "path": "./src/plugins/visualize/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_editor/tsconfig.json" }, - { "path": "./x-pack/plugins/actions/tsconfig.json" }, - { "path": "./x-pack/plugins/alerting/tsconfig.json" }, - { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/canvas/tsconfig.json" }, - { "path": "./x-pack/plugins/cases/tsconfig.json" }, - { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, - { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, - { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, - { "path": "./x-pack/plugins/event_log/tsconfig.json" }, - { "path": "./x-pack/plugins/features/tsconfig.json" }, - { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, - { "path": "./x-pack/plugins/fleet/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search/tsconfig.json" }, - { "path": "./x-pack/plugins/graph/tsconfig.json" }, - { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, - { "path": "./x-pack/plugins/infra/tsconfig.json" }, - { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./x-pack/plugins/lens/tsconfig.json" }, - { "path": "./x-pack/plugins/license_api_guard/tsconfig.json" }, - { "path": "./x-pack/plugins/license_management/tsconfig.json" }, - { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/lists/tsconfig.json" }, - { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps/tsconfig.json" }, - { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, - { "path": "./x-pack/plugins/ml/tsconfig.json" }, - { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, - { "path": "./x-pack/plugins/observability/tsconfig.json" }, - { "path": "./x-pack/plugins/osquery/tsconfig.json" }, - { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, - { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, - { "path": "./x-pack/plugins/security/tsconfig.json" }, - { "path": "./x-pack/plugins/security_solution/tsconfig.json" }, - { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, - { "path": "./x-pack/plugins/spaces/tsconfig.json" }, - { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, - { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, - { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./x-pack/plugins/timelines/tsconfig.json" }, - { "path": "./x-pack/plugins/transform/tsconfig.json" }, - { "path": "./x-pack/plugins/translations/tsconfig.json" }, - { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, - { "path": "./x-pack/plugins/index_management/tsconfig.json" }, - { "path": "./x-pack/plugins/watcher/tsconfig.json" }, - { "path": "./x-pack/plugins/rollup/tsconfig.json" }, - { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, - { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json"}, - { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json"}, - { "path": "./x-pack/plugins/uptime/tsconfig.json" }, - { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" } + { "path": "./x-pack/plugins/reporting/tsconfig.json" }, ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json deleted file mode 100644 index 1807a7014e38..000000000000 --- a/tsconfig.refs.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "include": [], - "references": [ - { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/advanced_settings/tsconfig.json" }, - { "path": "./src/plugins/apm_oss/tsconfig.json" }, - { "path": "./src/plugins/bfetch/tsconfig.json" }, - { "path": "./src/plugins/charts/tsconfig.json" }, - { "path": "./src/plugins/console/tsconfig.json" }, - { "path": "./src/plugins/dashboard/tsconfig.json" }, - { "path": "./src/plugins/data/tsconfig.json" }, - { "path": "./src/plugins/dev_tools/tsconfig.json" }, - { "path": "./src/plugins/discover/tsconfig.json" }, - { "path": "./src/plugins/embeddable/tsconfig.json" }, - { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "./src/plugins/expressions/tsconfig.json" }, - { "path": "./src/plugins/home/tsconfig.json" }, - { "path": "./src/plugins/inspector/tsconfig.json" }, - { "path": "./src/plugins/interactive_setup/tsconfig.json" }, - { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "./src/plugins/kibana_overview/tsconfig.json" }, - { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/legacy_export/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json" }, - { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/navigation/tsconfig.json" }, - { "path": "./src/plugins/newsfeed/tsconfig.json" }, - { "path": "./src/plugins/presentation_util/tsconfig.json" }, - { "path": "./src/plugins/region_map/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "./src/plugins/saved_objects/tsconfig.json" }, - { "path": "./src/plugins/security_oss/tsconfig.json" }, - { "path": "./src/plugins/share/tsconfig.json" }, - { "path": "./src/plugins/spaces_oss/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, - { "path": "./src/plugins/tile_map/tsconfig.json" }, - { "path": "./src/plugins/timelion/tsconfig.json" }, - { "path": "./src/plugins/ui_actions/tsconfig.json" }, - { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, - { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, - { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, - { "path": "./src/plugins/vis_type_table/tsconfig.json" }, - { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, - { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, - { "path": "./src/plugins/vis_type_pie/tsconfig.json" }, - { "path": "./src/plugins/visualizations/tsconfig.json" }, - { "path": "./src/plugins/visualize/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_editor/tsconfig.json" }, - { "path": "./test/tsconfig.json" }, - { "path": "./x-pack/plugins/actions/tsconfig.json" }, - { "path": "./x-pack/plugins/alerting/tsconfig.json" }, - { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/canvas/tsconfig.json" }, - { "path": "./x-pack/plugins/cases/tsconfig.json" }, - { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, - { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, - { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, - { "path": "./x-pack/plugins/event_log/tsconfig.json" }, - { "path": "./x-pack/plugins/features/tsconfig.json" }, - { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, - { "path": "./x-pack/plugins/fleet/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search/tsconfig.json" }, - { "path": "./x-pack/plugins/graph/tsconfig.json" }, - { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, - { "path": "./x-pack/plugins/infra/tsconfig.json" }, - { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./x-pack/plugins/lens/tsconfig.json" }, - { "path": "./x-pack/plugins/license_management/tsconfig.json" }, - { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/lists/tsconfig.json" }, - { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps/tsconfig.json" }, - { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, - { "path": "./x-pack/plugins/ml/tsconfig.json" }, - { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, - { "path": "./x-pack/plugins/observability/tsconfig.json" }, - { "path": "./x-pack/plugins/osquery/tsconfig.json" }, - { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, - { "path": "./x-pack/plugins/reporting/tsconfig.json" }, - { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, - { "path": "./x-pack/plugins/security/tsconfig.json" }, - { "path": "./x-pack/plugins/security_solution/tsconfig.json" }, - { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, - { "path": "./x-pack/plugins/spaces/tsconfig.json" }, - { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, - { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, - { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./x-pack/plugins/timelines/tsconfig.json" }, - { "path": "./x-pack/plugins/transform/tsconfig.json" }, - { "path": "./x-pack/plugins/translations/tsconfig.json" }, - { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, - { "path": "./x-pack/plugins/index_management/tsconfig.json" }, - { "path": "./x-pack/plugins/watcher/tsconfig.json" }, - { "path": "./x-pack/plugins/rollup/tsconfig.json" }, - { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, - { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json" }, - { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json" }, - { "path": "./x-pack/plugins/uptime/tsconfig.json" }, - { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" }, - { "path": "./x-pack/test/tsconfig.json" }, - ] -} diff --git a/tsconfig.types.json b/tsconfig.types.json index 86a45f6db169..54a26b6aca40 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -1,12 +1,12 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "incremental": false, - "declaration": true, + "composite": false, "outDir": "./target/types", "stripInternal": false, + "declaration": true, "emitDeclarationOnly": true, - "declarationMap": true + "declarationMap": true, }, "include": [ "src/core/server/index.ts", diff --git a/vars/tasks.groovy b/vars/tasks.groovy index e6ab3eaf92af..7ae8be25c93a 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -145,6 +145,14 @@ def functionalXpack(Map params = [:]) { // task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypressFirefox', './test/scripts/jenkins_security_solution_cypress_firefox.sh')) } } + + whenChanged([ + 'x-pack/plugins/apm/', + ]) { + if (githubPr.isPr()) { + task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) + } + } } } diff --git a/x-pack/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json index 95d42d40aceb..881c48029031 100644 --- a/x-pack/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target" + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +13,13 @@ ], "exclude": [], "references": [ - { "path": "../../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../plugins/alerting/tsconfig.json" }, + { "path": "../../plugins/triggers_actions_ui/tsconfig.json" }, + { "path": "../../plugins/features/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, ] } diff --git a/x-pack/examples/embedded_lens_example/tsconfig.json b/x-pack/examples/embedded_lens_example/tsconfig.json index 195db6effc5e..e1016a6c011a 100644 --- a/x-pack/examples/embedded_lens_example/tsconfig.json +++ b/x-pack/examples/embedded_lens_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,9 +13,9 @@ "exclude": [], "references": [ { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" } + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../plugins/lens/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, ] } diff --git a/x-pack/examples/reporting_example/tsconfig.json b/x-pack/examples/reporting_example/tsconfig.json index ef727b3368b1..4c4016911e0c 100644 --- a/x-pack/examples/reporting_example/tsconfig.json +++ b/x-pack/examples/reporting_example/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target" + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,7 +13,11 @@ ], "exclude": [], "references": [ - { "path": "../../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, + { "path": "../../plugins/reporting/tsconfig.json" }, ] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx index d6979749b7ef..2db7255e607d 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import type { SerializableRecord } from '@kbn/utility-types'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers'; -import { SerializableState } from '../../../../../../src/plugins/kibana_utils/common'; -export interface Config extends SerializableState { +export interface Config extends SerializableRecord { name: string; } diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index 567baca039d7..0282ecbdd173 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -16,6 +15,12 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" } + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/discover/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, + { "path": "../../plugins/dashboard_enhanced/tsconfig.json" }, + { "path": "../../plugins/ui_actions_enhanced/tsconfig.json" }, ] } diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index d5c1105c99ad..b2526d84a3ce 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerting/common/alert_navigation.ts index 7c9e428f9a09..6ac21232b51a 100644 --- a/x-pack/plugins/alerting/common/alert_navigation.ts +++ b/x-pack/plugins/alerting/common/alert_navigation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export interface AlertUrlNavigation { path: string; } diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts index 12ac90614264..ea36d0edd136 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { SanitizedAlert } from '../../common'; /** diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts index 77a15eda79ce..2ba3580745d5 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts @@ -75,35 +75,6 @@ test('creates an alerting authorization client with proper constructor arguments auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), getSpaceId: expect.any(Function), - exemptConsumerIds: [], - }); - - expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); - expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); -}); - -test('creates an alerting authorization client with proper constructor arguments when exemptConsumerIds are specified', async () => { - const factory = new AlertingAuthorizationClientFactory(); - factory.initialize({ - securityPluginSetup, - securityPluginStart, - ...alertingAuthorizationClientFactoryParams, - }); - const request = KibanaRequest.from(fakeRequest); - const { AlertingAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); - - factory.create(request, ['exemptConsumerA', 'exemptConsumerB']); - - const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization'); - expect(AlertingAuthorization).toHaveBeenCalledWith({ - request, - authorization: securityPluginStart.authz, - ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry, - features: alertingAuthorizationClientFactoryParams.features, - auditLogger: expect.any(AlertingAuthorizationAuditLogger), - getSpace: expect.any(Function), - getSpaceId: expect.any(Function), - exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); @@ -126,7 +97,6 @@ test('creates an alerting authorization client with proper constructor arguments auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), getSpaceId: expect.any(Function), - exemptConsumerIds: [], }); expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index 1df67ed8d4b7..27b2d92eba25 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -45,7 +45,7 @@ export class AlertingAuthorizationClientFactory { this.getSpaceId = options.getSpaceId; } - public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertingAuthorization { + public create(request: KibanaRequest): AlertingAuthorization { const { securityPluginSetup, securityPluginStart, features } = this; return new AlertingAuthorization({ authorization: securityPluginStart?.authz, @@ -57,7 +57,6 @@ export class AlertingAuthorizationClientFactory { auditLogger: new AlertingAuthorizationAuditLogger( securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), - exemptConsumerIds, }); } } diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 71ac9e48c729..7f5b06031c18 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -37,8 +37,6 @@ const realAuditLogger = new AlertingAuthorizationAuditLogger(); const getSpace = jest.fn(); const getSpaceId = () => 'space1'; -const exemptConsumerIds: string[] = []; - const mockAuthorizationAction = ( type: string, app: string, @@ -235,7 +233,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); expect(getSpace).toHaveBeenCalledWith(request); @@ -251,7 +248,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -275,7 +271,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -302,7 +297,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -359,7 +353,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -402,7 +395,7 @@ describe('AlertingAuthorization', () => { `); }); - test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is exempt', async () => { + test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is alerts', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -416,7 +409,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumer'], }); checkPrivileges.mockResolvedValueOnce({ @@ -427,7 +419,7 @@ describe('AlertingAuthorization', () => { await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', - consumer: 'exemptConsumer', + consumer: 'alerts', operation: WriteOperations.Create, entity: AlertingAuthorizationEntity.Rule, }); @@ -437,7 +429,7 @@ describe('AlertingAuthorization', () => { expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'exemptConsumer', + 'alerts', 'rule', 'create' ); @@ -458,14 +450,14 @@ describe('AlertingAuthorization', () => { "some-user", "myType", 0, - "exemptConsumer", + "alerts", "create", "rule", ] `); }); - test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is exempt', async () => { + test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is alerts', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -479,7 +471,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumer'], }); checkPrivileges.mockResolvedValueOnce({ @@ -490,7 +481,7 @@ describe('AlertingAuthorization', () => { await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', - consumer: 'exemptConsumer', + consumer: 'alerts', operation: WriteOperations.Update, entity: AlertingAuthorizationEntity.Alert, }); @@ -500,7 +491,7 @@ describe('AlertingAuthorization', () => { expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'exemptConsumer', + 'alerts', 'alert', 'update' ); @@ -521,7 +512,7 @@ describe('AlertingAuthorization', () => { "some-user", "myType", 0, - "exemptConsumer", + "alerts", "update", "alert", ] @@ -548,7 +539,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -614,7 +604,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -674,7 +663,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -733,7 +721,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -796,7 +783,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -855,7 +841,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -947,7 +932,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { filter, @@ -970,7 +954,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, @@ -1005,7 +988,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect( @@ -1013,18 +995,61 @@ describe('AlertingAuthorization', () => { await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }) ).filter ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); }); + test('throws if user has no privileges to any rule type', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), + authorized: false, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + auditLogger, + getSpace, + getSpaceId, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + await expect( + alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized some-user/find"`); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + }); test('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< @@ -1068,7 +1093,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( @@ -1142,7 +1166,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( @@ -1217,7 +1240,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { @@ -1270,7 +1292,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { filter } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Alert, @@ -1325,89 +1346,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - } - `); - }); - - test('augments a list of types with all features and exempt consumer ids when there is no authorization api', async () => { - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - features, - auditLogger, - getSpace, - getSpaceId, - exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1423,11 +1361,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { - "all": true, - "read": true, - }, - "exemptConsumerB": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1460,11 +1394,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { - "all": true, - "read": true, - }, - "exemptConsumerB": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1541,7 +1471,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1578,113 +1507,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - } - `); - }); - - test('augments a list of types with consumers and exempt consumer ids under which the operation is authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'rule', - 'create' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), - authorized: true, - }, - ], - }, - }); - - const alertAuthorization = new AlertingAuthorization({ - request, - authorization, - ruleTypeRegistry, - features, - auditLogger, - getSpace, - getSpaceId, - exemptConsumerIds: ['exemptConsumerA'], - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "exemptConsumerA": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1713,7 +1536,7 @@ describe('AlertingAuthorization', () => { `); }); - test('authorizes user under exempt consumers when they are authorized by the producer', async () => { + test('authorizes user under the `alerts` consumer when they are authorized by the producer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -1744,7 +1567,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumerA'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1760,7 +1582,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1850,7 +1672,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1866,6 +1687,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -1891,6 +1716,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, "myApp": Object { "all": false, "read": true, @@ -1960,7 +1789,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1976,6 +1804,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -2067,7 +1899,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -2142,7 +1973,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index dcde577bb5fd..101ab675bb55 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -8,8 +8,8 @@ import Boom from '@hapi/boom'; import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; -import { RuleTypeRegistry } from '../types'; +import { JsonObject } from '@kbn/utility-types'; +import { ALERTS_FEATURE_ID, RuleTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryRuleType } from '../rule_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; @@ -71,7 +71,6 @@ export interface ConstructorOptions { getSpace: (request: KibanaRequest) => Promise; getSpaceId: (request: KibanaRequest) => string | undefined; auditLogger: AlertingAuthorizationAuditLogger; - exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; } @@ -82,7 +81,6 @@ export class AlertingAuthorization { private readonly auditLogger: AlertingAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; - private readonly exemptConsumerIds: string[]; private readonly spaceId: string | undefined; constructor({ @@ -93,18 +91,12 @@ export class AlertingAuthorization { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.ruleTypeRegistry = ruleTypeRegistry; this.auditLogger = auditLogger; - // List of consumer ids that are exempt from privilege check. This should be used sparingly. - // An example of this is the Rules Management `consumer` as we don't want to have to - // manually authorize each rule type in the management UI. - this.exemptConsumerIds = exemptConsumerIds; - this.spaceId = getSpaceId(request); this.featuresIds = getSpace(request) @@ -132,7 +124,7 @@ export class AlertingAuthorization { this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { return featuresIds.size - ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { + ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], { read: true, all: true, }) @@ -185,8 +177,10 @@ export class AlertingAuthorization { ), }; - // Skip authorizing consumer if it is in the list of exempt consumer ids - const shouldAuthorizeConsumer = !this.exemptConsumerIds.includes(consumer); + // Skip authorizing consumer if consumer is the Rules Management consumer (`alerts`) + // This means that rules and their derivative alerts created in the Rules Management UI + // will only be subject to checking if user has access to the rule producer. + const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ @@ -199,8 +193,8 @@ export class AlertingAuthorization { requiredPrivilegesByScope.producer, ] : [ - // skip consumer privilege checks for exempt consumer ids as all rule types can - // be created for exempt consumers if user has producer level privileges + // skip consumer privilege checks under `alerts` as all rule types can + // be created under `alerts` if you have producer level privileges requiredPrivilegesByScope.producer, ], }); @@ -448,14 +442,12 @@ export class AlertingAuthorization { ruleType.authorizedConsumers[feature] ); - if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) { - // granting privileges under the producer automatically authorized exempt consumer IDs as well - this.exemptConsumerIds.forEach((exemptId: string) => { - ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[exemptId] - ); - }); + if (isAuthorizedAtProducerLevel) { + // granting privileges under the producer automatically authorized the Rules Management UI as well + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] + ); } authorizedRuleTypes.add(ruleType); } diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index 5ea15c4818a2..4cb790a5dc81 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -37,14 +37,16 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, 'space1' ) ).toEqual( - esKuery.fromKueryExpression(`((path.to.rule.id:myAppAlertType and consumer-field:(myApp)))`) + esKuery.fromKueryExpression( + `((path.to.rule_type_id:myAppAlertType and consumer-field:(myApp)))` + ) ); }); @@ -72,7 +74,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -80,7 +82,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp)))` ) ); }); @@ -144,7 +146,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -152,7 +154,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); }); @@ -199,7 +201,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.spaceIds', }, @@ -208,7 +210,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1))` ) ); }); @@ -255,7 +257,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.spaceIds', }, @@ -264,7 +266,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); }); @@ -293,7 +295,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -307,7 +309,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { should: [ { match: { - 'path.to.rule.id': 'myAppAlertType', + 'path.to.rule_type_id': 'myAppAlertType', }, }, ], @@ -355,7 +357,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -366,7 +368,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myAppAlertType' } }], minimum_should_match: 1, }, }, @@ -459,7 +461,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -473,7 +475,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myAppAlertType' } }], minimum_should_match: 1, }, }, @@ -516,7 +518,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myOtherAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myOtherAppAlertType' } }], minimum_should_match: 1, }, }, @@ -559,7 +561,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'mySecondAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'mySecondAppAlertType' } }], minimum_should_match: 1, }, }, @@ -611,7 +613,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, @@ -629,7 +631,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, @@ -645,7 +647,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -660,7 +662,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index 32b3bcc86657..c8000d989267 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -6,7 +6,7 @@ */ import { remove } from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, nodeBuilder, toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import { RegistryAlertTypeWithAuth } from './alerting_authorization'; diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts index f5455d1a6309..2874f9567231 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; type RenameAlertToRule = K extends `alertTypeId` ? `ruleTypeId` diff --git a/x-pack/plugins/alerting/server/rules_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_client_factory.test.ts index df59205cf10b..188ec652f40c 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.test.ts @@ -21,7 +21,6 @@ import { securityMock } from '../../security/server/mocks'; import { PluginStartContract as ActionsStartContract } from '../../actions/server'; import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { LegacyAuditLogger } from '../../security/server'; -import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; import { alertingAuthorizationMock } from './authorization/alerting_authorization.mock'; import { alertingAuthorizationClientFactoryMock } from './alerting_authorization_client_factory.mock'; @@ -105,9 +104,7 @@ test('creates an alerts client with proper constructor arguments when security i includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ - ALERTS_FEATURE_ID, - ]); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request); expect(rulesClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith( request @@ -148,9 +145,7 @@ test('creates an alerts client with proper constructor arguments', async () => { includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ - ALERTS_FEATURE_ID, - ]); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request); expect(jest.requireMock('./rules_client').RulesClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, diff --git a/x-pack/plugins/alerting/server/rules_client_factory.ts b/x-pack/plugins/alerting/server/rules_client_factory.ts index 336c8e6de20e..7961d3761d3e 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.ts @@ -19,7 +19,6 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve import { TaskManagerStartContract } from '../../task_manager/server'; import { IEventLogClientService } from '../../../plugins/event_log/server'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; -import { ALERTS_FEATURE_ID } from '../common'; export interface RulesClientFactoryOpts { logger: Logger; taskManager: TaskManagerStartContract; @@ -87,7 +86,7 @@ export class RulesClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization: this.authorization.create(request, [ALERTS_FEATURE_ID]), + authorization: this.authorization.create(request), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 9403c0c28c15..512f4618792f 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -970,6 +970,48 @@ describe('successful migrations', () => { }); }); }); + + describe('7.14.1', () => { + test('security solution author field is migrated to array if it is undefined', () => { + const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: {}, + }); + + expect(migration7141(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + params: { + author: [], + }, + }, + }); + }); + + test('security solution author field does not override existing values if they exist', () => { + const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + author: ['author 1'], + }, + }); + + expect(migration7141(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + params: { + note: 'some note', + author: ['author 1'], + }, + }, + }); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 9f6adeb27083..944acbdca018 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -85,11 +85,18 @@ export function getMigrations( pipeMigrations(removeNullsFromSecurityRules) ); + const migrationSecurityRules714 = createEsoMigration( + encryptedSavedObjects, + (doc): doc is SavedObjectUnsanitizedDoc => isSecuritySolutionRule(doc), + pipeMigrations(removeNullAuthorFromSecurityRules) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), + '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), }; } @@ -432,6 +439,34 @@ function removeNullsFromSecurityRules( }; } +/** + * The author field was introduced later and was not part of the original rules. We overlooked + * the filling in the author field as an empty array in an earlier upgrade routine from + * 'removeNullsFromSecurityRules' during the 7.13.0 upgrade. Since we don't change earlier migrations, + * but rather only move forward with the "arrow of time" we are going to upgrade and fix + * it if it is missing for anyone in 7.14.0 and above release. Earlier releases if we want to fix them, + * would have to be modified as a "7.13.1", etc... if we want to fix it there. + * @param doc The document that is not migrated and contains a "null" or "undefined" author field + * @returns The document with the author field fleshed in. + */ +function removeNullAuthorFromSecurityRules( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { params }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + params: { + ...params, + author: params.author != null ? params.author : [], + }, + }, + }; +} + function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 86ab00faeb5a..a822bd776134 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 52f1c3b44d79..141d94e2b168 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -75,6 +75,8 @@ exports[`Error HOST_NAME 1`] = `"my hostname"`; exports[`Error HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Error HOSTNAME 1`] = `undefined`; + exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Error HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`; @@ -314,6 +316,8 @@ exports[`Span HOST_NAME 1`] = `undefined`; exports[`Span HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Span HOSTNAME 1`] = `undefined`; + exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`; @@ -553,6 +557,8 @@ exports[`Transaction HOST_NAME 1`] = `"my hostname"`; exports[`Transaction HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Transaction HOSTNAME 1`] = `undefined`; + exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; exports[`Transaction HTTP_RESPONSE_STATUS_CODE 1`] = `200`; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 9476396a7aef..fa706b7d8cb3 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -71,7 +71,7 @@ export const ALERT_TYPES_CONFIG: Record< }, [AlertType.TransactionErrorRate]: { name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { - defaultMessage: 'Transaction error rate threshold', + defaultMessage: 'Failed transaction rate threshold', }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 82a592cd7e4d..d1f07c28bc80 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -114,6 +114,7 @@ export const LABEL_NAME = 'labels.name'; export const HOST = 'host'; export const HOST_NAME = 'host.hostname'; +export const HOSTNAME = 'host.name'; export const HOST_OS_PLATFORM = 'host.os.platform'; export const CONTAINER_ID = 'container.id'; export const KUBERNETES = 'kubernetes'; diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 6e1fd115aace..bb697f098433 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -32,6 +32,7 @@ export interface SearchServiceParams { export interface SearchServiceFetchParams extends SearchServiceParams { index: string; + includeFrozen?: boolean; } export interface SearchServiceValue { @@ -50,5 +51,4 @@ export interface AsyncSearchProviderProgress { loadedFieldCanditates: number; loadedFieldValuePairs: number; loadedHistograms: number; - getOverallProgress: () => number; } diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index a068e6ca7f38..b060f1aa6e00 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -13,6 +13,8 @@ import { asDecimalOrInteger, asInteger, asDecimal } from './formatters'; import { TimeUnit } from './datetime'; import { Maybe } from '../../../typings/common'; import { isFiniteNumber } from '../is_finite_number'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { ThroughputUnit } from '../../../server/lib/helpers/calculate_throughput'; interface FormatterOptions { defaultValue?: string; @@ -161,10 +163,15 @@ export function asTransactionRate(value: Maybe) { } return i18n.translate('xpack.apm.transactionRateLabel', { - defaultMessage: `{value} tpm`, - values: { - value: displayedValue, - }, + defaultMessage: `{displayedValue} tpm`, + values: { displayedValue }, + }); +} + +export function asExactTransactionRate(value: number, unit: ThroughputUnit) { + return i18n.translate('xpack.apm.exactTransactionRateLabel', { + defaultMessage: `{value} { unit, select, minute {tpm} other {tps} }`, + values: { value: asDecimalOrInteger(value), unit }, }); } diff --git a/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts index 4d095a79394a..9300ec61fe42 100644 --- a/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts +++ b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts @@ -18,10 +18,7 @@ export function offsetPreviousPeriodCoordinates({ if (!previousPeriodTimeseries?.length) { return []; } - const currentPeriodStart = currentPeriodTimeseries?.length - ? currentPeriodTimeseries[0].x - : 0; - + const currentPeriodStart = currentPeriodTimeseries?.[0].x ?? 0; const dateDiff = currentPeriodStart - previousPeriodTimeseries[0].x; return previousPeriodTimeseries.map(({ x, y }) => { diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index 4ea7c72fbc9a..0028f40a68d9 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -1,10 +1,5 @@ Feature: CSM Dashboard - Scenario: Service name filter - Given a user browses the APM UI application for RUM Data - When the user changes the selected service name - Then it displays relevant client metrics - Scenario: Client metrics When a user browses the APM UI application for RUM Data Then should have correct client metrics @@ -30,11 +25,6 @@ Feature: CSM Dashboard Then should display percentile for page load chart And should display tooltip on hover - Scenario: Breakdown filter - Given a user clicks the page load breakdown filter - When the user selected the breakdown - Then breakdown series should appear in chart - Scenario: Search by url filter focus When a user clicks inside url search field Then it displays top pages in the suggestion popover diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index c4587349c7ad..2560a15df922 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": ["tmp"], - "include": ["./**/*"], + "include": ["**/*"], + "exclude": ["tmp", "target/**/*"], "compilerOptions": { + "outDir": "target/types", "types": ["cypress", "node"] } } diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/config.ts index 38c196fbe70c..4cb218fd2475 100644 --- a/x-pack/plugins/apm/ftr_e2e/config.ts +++ b/x-pack/plugins/apm/ftr_e2e/config.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { resolve } from 'path'; - import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts index 106c380b4320..3f7e01be831f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -describe('APM depp links', () => { +describe('APM deep links', () => { before(() => { cy.loginAsReadOnlyUser(); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index d25251f457e3..76461d49ba01 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -7,23 +7,26 @@ import url from 'url'; import archives_metadata from '../../fixtures/es_archiver/archives_metadata'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const servicesPath = '/app/apm/services'; -const baseUrl = url.format({ - pathname: servicesPath, +const serviceInventoryHref = url.format({ + pathname: '/app/apm/services', query: { rangeFrom: start, rangeTo: end }, }); +const apisToIntercept = [ + { + endpoint: '/api/apm/service', + name: 'servicesMainStatistics', + }, + { + endpoint: '/api/apm/services/detailed_statistics', + name: 'servicesDetailedStatistics', + }, +]; + describe('Home page', () => { - before(() => { - esArchiverLoad('apm_8.0.0'); - }); - after(() => { - esArchiverUnload('apm_8.0.0'); - }); beforeEach(() => { cy.loginAsReadOnlyUser(); }); @@ -34,12 +37,12 @@ describe('Home page', () => { 'include', 'app/apm/services?rangeFrom=now-15m&rangeTo=now' ); - cy.get('.euiTabs .euiTab-isSelected').contains('Services'); }); - it('includes services with only metric documents', () => { + // Flaky + it.skip('includes services with only metric documents', () => { cy.visit( - `${baseUrl}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` + `${serviceInventoryHref}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` ); cy.contains('opbeans-python'); cy.contains('opbeans-java'); @@ -47,16 +50,28 @@ describe('Home page', () => { }); describe('navigations', () => { - it('navigates to service overview page with transaction type', () => { - const kuery = encodeURIComponent( - 'transaction.name : "taskManager markAvailableTasksAsClaimed"' - ); - cy.visit(`${baseUrl}&kuery=${kuery}`); - cy.contains('taskManager'); - cy.contains('kibana').click(); + /* + This test is flaky, there's a problem with EuiBasicTable, that it blocks any action while loading is enabled. + So it might fail to click on the service link. + */ + it.skip('navigates to service overview page with transaction type', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); + }); + + cy.visit(serviceInventoryHref); + + cy.contains('Services'); + + cy.wait('@servicesMainStatistics', { responseTimeout: 10000 }); + cy.wait('@servicesDetailedStatistics', { responseTimeout: 10000 }); + + cy.get('[data-test-subj="serviceLink_rum-js"]').then((element) => { + element[0].click(); + }); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'page-load' ); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts index d253a290f4a5..f124b3818c19 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -10,51 +10,51 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/kibana/overview'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-node/overview', query: { rangeFrom: start, rangeTo: end }, }); const apisToIntercept = [ { - endpoint: '/api/apm/services/kibana/transactions/charts/latency', - as: 'latencyChartRequest', + endpoint: '/api/apm/services/opbeans-node/transactions/charts/latency', + name: 'latencyChartRequest', }, { - endpoint: '/api/apm/services/kibana/throughput', - as: 'throughputChartRequest', + endpoint: '/api/apm/services/opbeans-node/throughput', + name: 'throughputChartRequest', }, { - endpoint: '/api/apm/services/kibana/transactions/charts/error_rate', - as: 'errorRateChartRequest', + endpoint: '/api/apm/services/opbeans-node/transactions/charts/error_rate', + name: 'errorRateChartRequest', }, { endpoint: - '/api/apm/services/kibana/transactions/groups/detailed_statistics', - as: 'transactionGroupsDetailedRequest', + '/api/apm/services/opbeans-node/transactions/groups/detailed_statistics', + name: 'transactionGroupsDetailedRequest', }, { endpoint: - '/api/apm/services/kibana/service_overview_instances/detailed_statistics', - as: 'instancesDetailedRequest', + '/api/apm/services/opbeans-node/service_overview_instances/detailed_statistics', + name: 'instancesDetailedRequest', }, { endpoint: - '/api/apm/services/kibana/service_overview_instances/main_statistics', - as: 'instancesMainStatisticsRequest', + '/api/apm/services/opbeans-node/service_overview_instances/main_statistics', + name: 'instancesMainStatisticsRequest', }, { - endpoint: '/api/apm/services/kibana/error_groups/main_statistics', - as: 'errorGroupsMainStatisticsRequest', + endpoint: '/api/apm/services/opbeans-node/error_groups/main_statistics', + name: 'errorGroupsMainStatisticsRequest', }, { - endpoint: '/api/apm/services/kibana/transaction/charts/breakdown', - as: 'transactonBreakdownRequest', + endpoint: '/api/apm/services/opbeans-node/transaction/charts/breakdown', + name: 'transactonBreakdownRequest', }, { - endpoint: '/api/apm/services/kibana/transactions/groups/main_statistics', - as: 'transactionsGroupsMainStatisticsRequest', + endpoint: + '/api/apm/services/opbeans-node/transactions/groups/main_statistics', + name: 'transactionsGroupsMainStatisticsRequest', }, ]; @@ -70,50 +70,46 @@ describe('Service overview - header filters', () => { }); describe('Filtering by transaction type', () => { it('changes url when selecting different value', () => { - cy.visit(baseUrl); - cy.contains('Kibana'); + cy.visit(serviceOverviewHref); + cy.contains('opbeans-node'); cy.url().should('not.include', 'transactionType'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select( - 'taskManager' - ); - cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.url().should('include', 'transactionType=Worker'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'Worker' ); }); it('calls APIs with correct transaction type', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); - cy.contains('Kibana'); + cy.visit(serviceOverviewHref); + cy.contains('opbeans-node'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' ); cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + apisIntercepted: apisToIntercept.map(({ name }) => `@${name}`), value: 'transactionType=request', }); - cy.get('[data-test-subj="headerFilterTransactionType"]').select( - 'taskManager' - ); - cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.url().should('include', 'transactionType=Worker'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'Worker' ); cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), - value: 'transactionType=taskManager', + apisIntercepted: apisToIntercept.map(({ name }) => `@${name}`), + value: 'transactionType=Worker', }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts index 2d76dfe977ef..40a08035f521 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -11,9 +11,8 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-java/overview', query: { rangeFrom: start, rangeTo: end }, }); @@ -21,22 +20,22 @@ const apisToIntercept = [ { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/main_statistics', - as: 'instancesMainRequest', + name: 'instancesMainRequest', }, { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', - as: 'instancesDetailsRequest', + name: 'instancesDetailsRequest', }, { endpoint: - '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', - as: 'instanceDetailsRequest', + '/api/apm/services/opbeans-java/service_overview_instances/details/31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad', + name: 'instanceDetailsRequest', }, { endpoint: - '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', - as: 'instanceDetailsRequest', + '/api/apm/services/opbeans-java/service_overview_instances/details/31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad', + name: 'instanceDetailsRequest', }, ]; @@ -46,7 +45,7 @@ describe('Instances table', () => { }); describe('when data is not loaded', () => { it('shows empty message', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( 'No items found' @@ -62,18 +61,19 @@ describe('Instances table', () => { esArchiverUnload('apm_8.0.0'); }); const serviceNodeName = - '02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c'; + '31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad'; it('has data in the table', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.contains(serviceNodeName); }); - it('shows instance details', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + // For some reason the details panel is not opening after clicking on the button. + it.skip('shows instance details', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); @@ -88,12 +88,13 @@ describe('Instances table', () => { cy.contains('Service'); }); }); - it('shows actions available', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + // For some reason the tooltip is not opening after clicking on the button. + it.skip('shows actions available', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index c3b4d979829f..7c5d5988c9bf 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -38,8 +38,7 @@ describe('Service Overview', () => { 'have.value', 'Worker' ); - - cy.get('[data-test-subj="tab_transactions"]').click(); + cy.contains('Transactions').click(); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'Worker' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts index 136328603a9d..de05cc3abb92 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts @@ -12,7 +12,7 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; -const baseUrl = url.format({ +const serviceOverviewHref = url.format({ pathname: serviceOverviewPath, query: { rangeFrom: start, rangeTo: end }, }); @@ -20,29 +20,29 @@ const baseUrl = url.format({ const apisToIntercept = [ { endpoint: '/api/apm/services/opbeans-java/transactions/charts/latency', - as: 'latencyChartRequest', + name: 'latencyChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/throughput', - as: 'throughputChartRequest', + name: 'throughputChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - as: 'errorRateChartRequest', + name: 'errorRateChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/transactions/groups/detailed_statistics', - as: 'transactionGroupsDetailedRequest', + name: 'transactionGroupsDetailedRequest', }, { endpoint: '/api/apm/services/opbeans-java/error_groups/detailed_statistics', - as: 'errorGroupsDetailedRequest', + name: 'errorGroupsDetailedRequest', }, { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', - as: 'instancesDetailedRequest', + name: 'instancesDetailedRequest', }, ]; @@ -64,7 +64,7 @@ describe('Service overview: Time Comparison', () => { describe('when comparison is toggled off', () => { it('disables select box', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); // Comparison is enabled by default @@ -76,17 +76,17 @@ describe('Service overview: Time Comparison', () => { }); it('calls APIs without comparison time range', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled'); const comparisonStartEnd = - 'comparisonStart=2020-12-08T13%3A26%3A03.865Z&comparisonEnd=2020-12-08T13%3A57%3A00.000Z'; + 'comparisonStart=2021-08-02T06%3A50%3A00.000Z&comparisonEnd=2021-08-02T07%3A20%3A15.910Z'; // When the page loads it fetches all APIs with comparison time range - cy.wait(apisToIntercept.map(({ as }) => `@${as}`)).then( + cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then( (interceptions) => { interceptions.map((interception) => { expect(interception.request.url).include(comparisonStartEnd); @@ -98,7 +98,7 @@ describe('Service overview: Time Comparison', () => { cy.contains('Comparison').click(); cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); // When comparison is disabled APIs are called withou comparison time range - cy.wait(apisToIntercept.map(({ as }) => `@${as}`)).then( + cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then( (interceptions) => { interceptions.map((interception) => { expect(interception.request.url).not.include(comparisonStartEnd); @@ -109,8 +109,8 @@ describe('Service overview: Time Comparison', () => { }); it('changes comparison type', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); cy.visit(serviceOverviewPath); cy.contains('opbeans-java'); @@ -131,18 +131,8 @@ describe('Service overview: Time Comparison', () => { cy.contains('Week before'); cy.changeTimeRange('Today'); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'have.value', - 'period' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'not.contain.text', - 'Day before' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'not.contain.text', - 'Week before' - ); + cy.contains('Day before'); + cy.contains('Week before'); cy.changeTimeRange('Last 24 hours'); cy.get('[data-test-subj="comparisonSelect"]').should('have.value', 'day'); @@ -177,8 +167,8 @@ describe('Service overview: Time Comparison', () => { }); it('hovers over throughput chart shows previous and current period', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); cy.visit( url.format({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts index fc17d1975d63..eaa0ee9e4d65 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts @@ -11,9 +11,8 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/opbeans-node/transactions'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-node/transactions', query: { rangeFrom: start, rangeTo: end }, }); @@ -27,8 +26,8 @@ describe('Transactions Overview', () => { beforeEach(() => { cy.loginAsReadOnlyUser(); }); - it('persists transaction type selected when clicking on Overview tab', () => { - cy.visit(baseUrl); + it('persists transaction type selected when navigating to Overview tab', () => { + cy.visit(serviceOverviewHref); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' @@ -38,8 +37,7 @@ describe('Transactions Overview', () => { 'have.value', 'Worker' ); - - cy.get('[data-test-subj="tab_overview"]').click(); + cy.get('a[href*="/app/apm/services/opbeans-node/overview"]').click(); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'Worker' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index 0a13caa1a665..a6027367d786 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -7,12 +7,21 @@ import Url from 'url'; import cypress from 'cypress'; -import childProcess from 'child_process'; import { FtrProviderContext } from './ftr_provider_context'; import archives_metadata from './cypress/fixtures/es_archiver/archives_metadata'; +import { createKibanaUserRole } from '../scripts/kibana-security/create_kibana_user_role'; export async function cypressRunTests({ getService }: FtrProviderContext) { - await cypressStart(getService, cypress.run); + try { + const result = await cypressStart(getService, cypress.run); + + if (result && (result.status === 'failed' || result.totalFailed > 0)) { + process.exit(1); + } + } catch (error) { + console.error('errors: ', error); + process.exit(1); + } } export async function cypressOpenTests({ getService }: FtrProviderContext) { @@ -35,20 +44,22 @@ async function cypressStart( }); // Creates APM users - childProcess.execSync( - `node ../scripts/setup-kibana-security.js --role-suffix e2e_tests --username ${config.get( - 'servers.elasticsearch.username' - )} --password ${config.get( - 'servers.elasticsearch.password' - )} --kibana-url ${kibanaUrl}` - ); - - await cypressExecution({ + await createKibanaUserRole({ + elasticsearch: { + username: config.get('servers.elasticsearch.username'), + password: config.get('servers.elasticsearch.password'), + }, + kibana: { + hostname: kibanaUrl, + roleSuffix: 'e2e_tests', + }, + }); + + return cypressExecution({ config: { baseUrl: kibanaUrl }, env: { START_DATE: start, END_DATE: end, - ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), KIBANA_URL: kibanaUrl, }, }); diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index 94b2b024ca4c..84a66afe4588 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -1,16 +1,22 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [ - "tmp" - ], "include": [ - "./**/*" + "**/*" + ], + "exclude": [ + "tmp", + "target/**/*" ], "compilerOptions": { + "outDir": "target/types", "types": [ "cypress", "node", "cypress-real-events" ] - } -} \ No newline at end of file + }, + "references": [ + { "path": "../../../test/tsconfig.json" }, + { "path": "../../../../test/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index 3c79a2475bf2..a06520f1c5bf 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { asInteger } from '../../../../common/utils/formatters'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -47,6 +48,7 @@ export function ErrorCountAlertTrigger(props: Props) { threshold: 25, windowSize: 1, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 2e16599c0271..8480fd276cb7 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -9,7 +9,10 @@ import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; -import { getEnvironmentLabel } from '../../../common/environment_filter_values'; +import { + ENVIRONMENT_ALL, + getEnvironmentLabel, +} from '../../../common/environment_filter_values'; import { PopoverExpression } from './service_alert_trigger/popover_expression'; const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { @@ -42,7 +45,7 @@ export function EnvironmentField({ // "1" means "All" is the only option and we should not show a select. if (options.length === 1) { - return ; + return ; } return ( diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index ff06c845ba4d..12102a294bf9 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -8,11 +8,17 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { stringify } from 'querystring'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { AlertType } from '../../../common/alert_types'; @@ -22,6 +28,10 @@ const SERVICE_ENVIRONMENT = 'service.environment'; const SERVICE_NAME = 'service.name'; const TRANSACTION_TYPE = 'transaction.type'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; + const format = ({ pathname, query, @@ -154,7 +164,7 @@ export function registerApmAlerts( reason: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.reason', { - defaultMessage: `Transaction error rate is greater than {threshold} (current value is {measured}) for {serviceName}`, + defaultMessage: `Failed transactions rate is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { threshold: asPercent(fields[ALERT_EVALUATION_THRESHOLD], 100), measured: asPercent(fields[ALERT_EVALUATION_VALUE], 100), diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index c54f32d80581..2a73cba0a63d 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -9,6 +9,7 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { defaults, map, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; @@ -90,6 +91,8 @@ export function TransactionDurationAlertTrigger(props: Props) { threshold: 1500, windowSize: 5, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, + transactionType: transactionTypes[0], } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx index 97ce73a74632..519b18cd3a6b 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -60,6 +61,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { windowSize: 15, windowUnit: 'm', anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, + environment: ENVIRONMENT_ALL.value, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index 6118f99e4bd9..8f2e212a2268 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -7,6 +7,7 @@ import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; @@ -57,6 +58,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { threshold: 30, windowSize: 5, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, } ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 0bd873bd7064..4e6544a20f30 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -11,11 +11,11 @@ import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, HeaderMenuPortal, + SeriesUrl, } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppMountParameters } from '../../../../../../../../src/core/public'; -import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { defaultMessage: 'Analyze data', @@ -38,22 +38,15 @@ export function UXActionMenu({ services: { http }, } = useKibana(); const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom, serviceName } = urlParams; + const { rangeTo, rangeFrom } = urlParams; const uxExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'ux', - name: `${serviceName}-page-views`, - time: { from: rangeFrom!, to: rangeTo! }, - reportDefinitions: { - [SERVICE_NAME]: serviceName ? [serviceName] : [], - }, - selectedMetricField: 'Records', - }, - ], + 'ux-series': ({ + dataType: 'ux', + isNew: true, + time: { from: rangeFrom, to: rangeTo }, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); @@ -67,7 +60,6 @@ export function UXActionMenu({ {ANALYZE_MESSAGE}

}>

{i18n.translate( - 'xpack.apm.backendDetailErrorRateChartTitle', - { defaultMessage: 'Error rate' } + 'xpack.apm.backendDetailFailedTransactionRateChartTitle', + { defaultMessage: 'Failed transaction rate' } )}

- + diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index c89526b33208..eff744fa45ae 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNodeName, NodeType } from '../../../../../common/connections'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -16,6 +17,7 @@ import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time import { DependenciesTable } from '../../../shared/dependencies_table'; import { BackendLink } from '../../../shared/backend_link'; import { DependenciesTableServiceMapLink } from '../../../shared/dependencies_table/dependencies_table_service_map_link'; +import { useUiTracker } from '../../../../../../observability/public'; export function BackendInventoryDependenciesTable() { const { @@ -27,6 +29,9 @@ export function BackendInventoryDependenciesTable() { } = useApmParams('/backends'); const router = useApmRouter(); + + const trackEvent = useUiTracker(); + const serviceMapLink = router.link('/service-map', { query: { rangeFrom, @@ -79,6 +84,13 @@ export function BackendInventoryDependenciesTable() { rangeFrom, rangeTo, }} + onClick={() => { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'backend_inventory_to_backend_detail', + }); + }} /> ); diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index ad2699604279..0bb7ac3c17c4 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -54,7 +54,7 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi const errorRateTab = { key: 'errorRate', label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', }), component: ErrorCorrelations, }; diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 24257bcefa7f..fa458b95d0d8 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -19,7 +19,9 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React, { Suspense, useState } from 'react'; -import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; @@ -29,6 +31,8 @@ import { getAlertAnnotations } from '../../../shared/charts/helper/get_alert_ann import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { LazyAlertsFlyout } from '../../../../../../observability/public'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; + type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>; interface FormattedBucket { @@ -124,7 +128,7 @@ export function ErrorDistribution({ distribution, title }: Props) { /> {getAlertAnnotations({ alerts: alerts?.filter( - (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount + (alert) => alert[ALERT_RULE_TYPE_ID]?.[0] === AlertType.ErrorCount ), chartStartTime: buckets[0]?.x0, getFormatter, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 3b7dea1e6406..cf4cc6886597 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -101,6 +101,7 @@ export function ErrorGroupDetails() { const { path: { groupId }, + query: { rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/errors/:groupId'); useBreadcrumb({ @@ -110,6 +111,10 @@ export function ErrorGroupDetails() { serviceName, groupId, }, + query: { + rangeFrom, + rangeTo, + }, }), }); diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx index a33b0db7c4ba..1ce6d5475471 100644 --- a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx @@ -18,13 +18,14 @@ export function ServiceDependenciesBreakdownChart({ }: { height: number; }) { - const { start, end } = useTimeRange(); const { serviceName } = useApmServiceContext(); const { - query: { kuery, environment }, + query: { kuery, environment, rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/dependencies'); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { data, status } = useFetcher( (callApmApi) => { return callApmApi({ diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 290e71a23bbb..2a59a98ee31f 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -15,6 +15,8 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; @@ -155,6 +157,7 @@ function useServicesFetcher() { export function ServiceInventory() { const { core } = useApmPluginContext(); + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { servicesData, servicesStatus, @@ -189,6 +192,11 @@ export function ServiceInventory() { setUserHasDismissedCallout(true)} /> )} + {fallbackToTransactions && ( + + + + )} { it('should render services, when list is not empty', async () => { // mock rest requests - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - { - serviceName: 'My Go Service', - agentName: 'go', - transactionsPerMinute: 400, - errorsPerMinute: 500, - avgResponseTime: 600, - environments: [], - severity: ServiceHealthStatus.healthy, - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + healthStatus: ServiceHealthStatus.warning, + }, + { + serviceName: 'My Go Service', + agentName: 'go', + transactionsPerMinute: 400, + errorsPerMinute: 500, + avgResponseTime: 600, + environments: [], + severity: ServiceHealthStatus.healthy, + }, + ], + }); const { container, findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); await findByText('My Python Service'); expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); }); it('should render getting started message, when list is empty and no historical data is found', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: false, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: false, + items: [], + }); const { findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); // wait for elements to be rendered const gettingStartedMessage = await findByText( @@ -153,16 +157,18 @@ describe('ServiceInventory', () => { }); it('should render empty message, when list is empty and historical data is found', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [], + }); const { findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); const noServicesText = await findByText('No services found'); expect(noServicesText).not.toBeEmptyDOMElement(); @@ -170,16 +176,18 @@ describe('ServiceInventory', () => { describe('when legacy data is found', () => { it('renders an upgrade migration notification', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: true, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: true, + hasHistoricalData: true, + items: [], + }); render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(addWarning).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -191,16 +199,18 @@ describe('ServiceInventory', () => { describe('when legacy data is not found', () => { it('does not render an upgrade migration notification', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [], + }); render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(addWarning).not.toHaveBeenCalled(); }); @@ -208,25 +218,27 @@ describe('ServiceInventory', () => { describe('when ML data is not found', () => { it('does not render the health column', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + }, + ], + }); const { queryByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(queryByText('Health')).toBeNull(); }); @@ -234,26 +246,28 @@ describe('ServiceInventory', () => { describe('when ML data is found', () => { it('renders the health column', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + healthStatus: ServiceHealthStatus.warning, + }, + ], + }); const { queryAllByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(queryAllByText('Health').length).toBeGreaterThan(1); }); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx index af2cde7f861c..7a4721407e69 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React from 'react'; import { Coordinate } from '../../../../../typings/timeseries'; import { SparkPlot } from '../../../shared/charts/spark_plot'; @@ -14,18 +15,32 @@ export function ServiceListMetric({ series, valueLabel, comparisonSeries, + hideSeries = false, }: { color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; series?: Coordinate[]; comparisonSeries?: Coordinate[]; valueLabel: React.ReactNode; + hideSeries?: boolean; }) { + if (!hideSeries) { + return ( + + ); + } + return ( - + + + + {valueLabel} + + + ); } 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 c2ba67356fb6..32b77c58c38f 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 @@ -13,10 +13,14 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { TypeOf } from '@kbn/typed-react-router-config'; import { orderBy } from 'lodash'; import React, { useMemo } from 'react'; import { ValuesType } from 'utility-types'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { + BreakPoints, + useBreakPoints, +} from '../../../../hooks/use_break_points'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { @@ -31,11 +35,13 @@ import { import { useApmParams } from '../../../../hooks/use_apm_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../utils/style'; +import { ApmRoutes } from '../../../routing/apm_route_config'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { ServiceLink } from '../../../shared/service_link'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; type Items = ServiceListAPIResponse['items']; @@ -47,13 +53,6 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const ToolTipWrapper = euiStyled.span` - width: 100%; - .apmServiceList__serviceNameTooltip { - width: 100%; - } -`; - const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.unknown, ServiceHealthStatus.healthy, @@ -65,11 +64,16 @@ export function getServiceColumns({ query, showTransactionTypeColumn, comparisonData, + breakPoints, }: { - query: Record; + query: TypeOf['query']; showTransactionTypeColumn: boolean; + breakPoints: BreakPoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { + const { isSmall, isLarge, isXl } = breakPoints; + const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge; + const showWhenSmallOrGreaterThanXL = isSmall || !isXl; return [ { field: 'healthStatus', @@ -94,34 +98,38 @@ export function getServiceColumns({ width: '40%', sortable: true, render: (_, { serviceName, agentName }) => ( - - + - - - ), - }, - { - field: 'environments', - name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { - defaultMessage: 'Environment', - }), - width: `${unit * 10}px`, - sortable: true, - render: (_, { environments }) => ( - + } + /> ), }, - ...(showTransactionTypeColumn + ...(showWhenSmallOrGreaterThanLarge + ? [ + { + field: 'environments', + name: i18n.translate( + 'xpack.apm.servicesTable.environmentColumnLabel', + { + defaultMessage: 'Environment', + } + ), + width: `${unit * 10}px`, + sortable: true, + render: (_, { environments }) => ( + + ), + } as ITableColumn, + ] + : []), + ...(showTransactionTypeColumn && showWhenSmallOrGreaterThanXL ? [ { field: 'transactionType', @@ -147,12 +155,13 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.latency } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis1" valueLabel={asMillisecondDuration(latency || 0)} /> ), align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, { field: 'throughput', @@ -167,17 +176,18 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.throughput } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis0" valueLabel={asTransactionRate(throughput)} /> ), align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, { field: 'transactionErrorRate', name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { - defaultMessage: 'Error rate %', + defaultMessage: 'Failed transaction rate', }), sortable: true, dataType: 'number', @@ -191,13 +201,14 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.transactionErrorRate } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis7" valueLabel={valueLabel} /> ); }, align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, ]; } @@ -215,6 +226,7 @@ export function ServiceList({ comparisonData, isLoading, }: Props) { + const breakPoints = useBreakPoints(); const displayHealthStatus = items.some((item) => 'healthStatus' in item); const showTransactionTypeColumn = items.some( @@ -227,8 +239,13 @@ export function ServiceList({ const serviceColumns = useMemo( () => - getServiceColumns({ query, showTransactionTypeColumn, comparisonData }), - [query, showTransactionTypeColumn, comparisonData] + getServiceColumns({ + query, + showTransactionTypeColumn, + comparisonData, + breakPoints, + }), + [query, showTransactionTypeColumn, comparisonData, breakPoints] ); 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 70a6191a1d6b..51636e938f8c 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 @@ -7,6 +7,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { BreakPoints } from '../../../../hooks/use_break_points'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; @@ -42,7 +43,12 @@ describe('ServiceList', () => { ).not.toThrowError(); }); - it('renders columns correctly', () => { + describe('responsive columns', () => { + const query = { + rangeFrom: 'now-15m', + rangeTo: 'now', + }; + const service: any = { serviceName: 'opbeans-python', agentName: 'python', @@ -59,17 +65,132 @@ describe('ServiceList', () => { timeseries: [], }, environments: ['test'], + transactionType: 'request', }; - const renderedColumns = getServiceColumns({ - query: {}, - showTransactionTypeColumn: false, - }).map((c) => c.render!(service[c.field!], service)); - - expect(renderedColumns[0]).toMatchInlineSnapshot(` - - `); + describe('when small', () => { + it('shows environment, transaction type and sparklines', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: true, + isLarge: true, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(7); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); + expect(renderedColumns[4]).toMatchInlineSnapshot(` + + `); + }); + }); + + describe('when Large', () => { + it('hides environment, transaction type and sparklines', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: true, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(5); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + }); + + describe('when XL', () => { + it('hides transaction type', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: false, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(6); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(` + + `); + }); + }); + + describe('when XXL', () => { + it('hides transaction type', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(7); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); + expect(renderedColumns[4]).toMatchInlineSnapshot(` + + `); + }); + }); + }); }); describe('without ML data', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx new file mode 100644 index 000000000000..8e642c1f27e1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -0,0 +1,104 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { LogStream } from '../../../../../infra/public'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +import { + CONTAINER_ID, + HOSTNAME, + POD_NAME, +} from '../../../../common/elasticsearch_fieldnames'; + +export function ServiceLogs() { + const { serviceName } = useApmServiceContext(); + const { + urlParams: { environment, kuery, start, end }, + } = useUrlParams(); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/infrastructure', + params: { + path: { serviceName }, + query: { + environment, + kuery, + start, + end, + }, + }, + }); + } + }, + [environment, kuery, serviceName, start, end] + ); + + const noInfrastructureData = useMemo(() => { + return ( + isEmpty(data?.serviceInfrastructure?.containerIds) && + isEmpty(data?.serviceInfrastructure?.hostNames) && + isEmpty(data?.serviceInfrastructure?.podNames) + ); + }, [data]); + + if (status === FETCH_STATUS.LOADING) { + return ( +
+ +
+ ); + } + + if (status === FETCH_STATUS.SUCCESS && noInfrastructureData) { + return ( + + {i18n.translate('xpack.apm.serviceLogs.noInfrastructureMessage', { + defaultMessage: 'There are no log messages to display.', + })} + + } + /> + ); + } + + return ( + + ); +} + +const getInfrastructureKQLFilter = ( + data?: APIReturnType<'GET /api/apm/services/{serviceName}/infrastructure'> +) => { + const containerIds = data?.serviceInfrastructure?.containerIds ?? []; + const hostNames = data?.serviceInfrastructure?.hostNames ?? []; + const podNames = data?.serviceInfrastructure?.podNames ?? []; + + return [ + ...containerIds.map((id) => `${CONTAINER_ID}: "${id}"`), + ...hostNames.map((id) => `${HOSTNAME}: "${id}"`), + ...podNames.map((id) => `${POD_NAME}: "${id}"`), + ].join(' or '); +}; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index e0fef269f3fa..43308cfdce9f 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -8,7 +8,9 @@ import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/typed-react-router-config'; +import { METRIC_TYPE } from '@kbn/analytics'; import React from 'react'; +import { useUiTracker } from '../../../../../../observability/public'; import { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -58,13 +60,26 @@ export function BackendContents({ nodeData }: ContentsProps) { >['query'], }); + const trackEvent = useUiTracker(); + return ( <> - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} + { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'service_map_to_backend_detail', + }); + }} + > {i18n.translate('xpack.apm.serviceMap.backendDetailsButtonText', { defaultMessage: 'Backend Details', })} diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index b486e5e19fb0..274e25b342d2 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -10,6 +10,7 @@ import { EuiButton, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useApmParams } from '../../../../hooks/use_apm_params'; import type { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -17,14 +18,29 @@ import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { AnomalyDetection } from './anomaly_detection'; import { StatsList } from './stats_list'; +import { useTimeRange } from '../../../../hooks/use_time_range'; export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { const apmRouter = useApmRouter(); const { - urlParams: { environment, start, end }, + urlParams: { environment }, } = useUrlParams(); + const { query } = useApmParams('/*'); + + if ( + !('rangeFrom' in query && 'rangeTo' in query) || + !query.rangeFrom || + !query.rangeTo + ) { + throw new Error('Expected rangeFrom and rangeTo to be set'); + } + + const { rangeFrom, rangeTo } = query; + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const serviceName = nodeData.id!; const { data = { transactionStats: {} } as NodeStats, status } = useFetcher( @@ -49,10 +65,12 @@ export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { const detailsUrl = apmRouter.link('/services/:serviceName', { path: { serviceName }, + query: { rangeFrom, rangeTo }, }); const focusUrl = apmRouter.link('/services/:serviceName/service-map', { path: { serviceName }, + query: { rangeFrom, rangeTo }, }); const { serviceAnomalyStats } = nodeData; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx index 88915b9bc9f3..b46b7a098617 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx @@ -104,7 +104,7 @@ export function StatsList({ data, isLoading }: StatsListProps) { }, { title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { - defaultMessage: 'Trans. error rate (avg.)', + defaultMessage: 'Failed transaction rate (avg.)', }), description: asPercent(avgErrorRate, 1, ''), }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 620eefda05b2..a261095fbe2f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -20,6 +20,8 @@ import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { TransactionsTable } from '../../shared/transactions_table'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -28,6 +30,7 @@ import { TransactionsTable } from '../../shared/transactions_table'; export const chartHeight = 288; export function ServiceOverview() { + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { agentName, serviceName } = useApmServiceContext(); // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we @@ -41,6 +44,11 @@ export function ServiceOverview() { + {fallbackToTransactions && ( + + + + )} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 5f77583143d7..b3c1afb32b0f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -140,6 +140,9 @@ describe('ServiceOverview', () => { 'GET /api/apm/services/{serviceName}/annotation/search': { annotations: [], }, + 'GET /api/apm/fallback_to_transactions': { + fallbackToTransactions: false, + }, }; /* eslint-enable @typescript-eslint/naming-convention */ 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 d9df9acf9ff6..085c69bb9eec 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 @@ -8,6 +8,8 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useUiTracker } from '../../../../../../observability/public'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNodeName, NodeType } from '../../../../../common/connections'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; @@ -54,6 +56,8 @@ export function ServiceOverviewDependenciesTable() { query, }); + const trackEvent = useUiTracker(); + const { data, status } = useFetcher( (callApmApi) => { if (!start || !end) { @@ -89,6 +93,13 @@ export function ServiceOverviewDependenciesTable() { rangeFrom, rangeTo, }} + onClick={() => { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'service_dependencies_to_backend_detail', + }); + }} /> ) : ( { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 1d6c538570f8..741491f87f78 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -5,10 +5,16 @@ * 2.0. */ -import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { + EuiPanel, + EuiTitle, + EuiIconTip, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { asTransactionRate } from '../../../../common/utils/formatters'; +import { asExactTransactionRate } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -22,6 +28,7 @@ import { const INITIAL_STATE = { currentPeriod: [], previousPeriod: [], + throughputUnit: 'minute' as const, }; export function ServiceOverviewThroughputChart({ @@ -111,20 +118,49 @@ export function ServiceOverviewThroughputChart({ return ( - -

- {i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { - defaultMessage: 'Throughput', - })} -

-
+ + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.throughtputChartTitle', + { defaultMessage: 'Throughput' } + )} + {data.throughputUnit === 'second' + ? i18n.translate( + 'xpack.apm.serviceOverview.throughtputPerSecondChartTitle', + { defaultMessage: ' (per second)' } + ) + : ''} +

+
+
+ + + + +
+ asExactTransactionRate(y, data.throughputUnit)} customTheme={comparisonChartTheme} />
diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index eed0750a9e39..76e537dc66c0 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -5,12 +5,15 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; import { TraceList } from './trace_list'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { @@ -18,6 +21,7 @@ const DEFAULT_RESPONSE: TracesAPIResponse = { }; export function TraceOverview() { + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { urlParams: { environment, kuery, start, end }, } = useUrlParams(); @@ -44,6 +48,14 @@ export function TraceOverview() { <> + {fallbackToTransactions && ( + + + + + + )} + { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'span_flyout_to_backend_detail', + }); + }} /> ), width: '25%', diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index a0c2108948f2..f521d93fa0cc 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Location } from 'history'; import React from 'react'; import { useLocation } from 'react-router-dom'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { IUrlParams } from '../../../context/url_params_context/types'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; @@ -40,6 +42,7 @@ function getRedirectLocation({ } export function TransactionOverview() { + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const location = useLocation(); const { urlParams } = useUrlParams(); const { transactionType, serviceName } = useApmServiceContext(); @@ -55,6 +58,16 @@ export function TransactionOverview() { return ( <> + {fallbackToTransactions && ( + <> + + + + + + + + )} diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 10f88d3e8b4c..1502038b378b 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -53,14 +53,24 @@ export const BackendInventoryTitle = i18n.translate( export const home = { path: '/', element: , - params: t.partial({ - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - environment: t.string, - kuery: t.string, - }), + params: t.type({ + query: t.intersection([ + t.partial({ + environment: t.string, + kuery: t.string, + }), + t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + ]), }), + defaults: { + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }, children: [ page({ path: '/services', diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 5771d21b77a3..5aeae224d0d5 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -22,6 +22,7 @@ import { ServiceMap } from '../../app/service_map'; import { TransactionDetails } from '../../app/transaction_details'; import { ServiceProfiling } from '../../app/service_profiling'; import { ServiceDependencies } from '../../app/service_dependencies'; +import { ServiceLogs } from '../../app/service_logs'; function page({ path, @@ -66,19 +67,29 @@ export const serviceDetail = { serviceName: t.string, }), }), - t.partial({ - query: t.partial({ - environment: t.string, - rangeFrom: t.string, - rangeTo: t.string, - comparisonEnabled: t.string, - comparisonType: t.string, - latencyAggregationType: t.string, - transactionType: t.string, - kuery: t.string, - }), + t.type({ + query: t.intersection([ + t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + t.partial({ + environment: t.string, + comparisonEnabled: t.string, + comparisonType: t.string, + latencyAggregationType: t.string, + transactionType: t.string, + kuery: t.string, + }), + ]), }), ]), + defaults: { + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }, children: [ page({ path: '/overview', @@ -223,6 +234,14 @@ export const serviceDetail = { hidden: true, }, }), + page({ + path: '/logs', + tab: 'logs', + title: i18n.translate('xpack.apm.views.logs.title', { + defaultMessage: 'Logs', + }), + element: , + }), page({ path: '/profiling', tab: 'profiling', diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx index c5ad2b100182..b8b0cfa3054d 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx @@ -38,7 +38,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -78,7 +78,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 5af6ea6cdc77..d8ff7fdf47c5 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -9,7 +9,10 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { createExploratoryViewUrl } from '../../../../../../observability/public'; +import { + createExploratoryViewUrl, + SeriesUrl, +} from '../../../../../../observability/public'; import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; import { isIosAgentName, @@ -18,7 +21,6 @@ import { import { SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, @@ -27,11 +29,13 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -function getEnvironmentDefinition(environment: string) { +function getEnvironmentDefinition(environment?: string) { switch (environment) { case ENVIRONMENT_ALL.value: return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; case ENVIRONMENT_NOT_DEFINED.value: + case undefined: + return {}; default: return { [SERVICE_ENVIRONMENT]: [environment] }; } @@ -47,26 +51,21 @@ export function AnalyzeDataButton() { if ( (isRumAgentName(agentName) || isIosAgentName(agentName)) && - rangeFrom && - canShowDashboard && - rangeTo + canShowDashboard ) { const href = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${serviceName}-response-latency`, - selectedMetricField: TRANSACTION_DURATION, - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...(environment ? getEnvironmentDefinition(environment) : {}), - }, - operationType: 'average', + 'apm-series': { + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportType: 'kpi-over-time', + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...getEnvironmentDefinition(environment), }, - ], + operationType: 'average', + isNew: true, + } as SeriesUrl, }, basepath ); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index d92d7a8d9492..d332048338cc 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -41,6 +41,7 @@ type Tab = NonNullable[0] & { | 'metrics' | 'nodes' | 'service-map' + | 'logs' | 'profiling'; hidden?: boolean; }; @@ -218,6 +219,18 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { defaultMessage: 'Service Map', }), }, + { + key: 'logs', + href: router.link('/services/:serviceName/logs', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.serviceLogsTabLabel', { + defaultMessage: 'Logs', + }), + hidden: + !agentName || isRumAgentName(agentName) || isIosAgentName(agentName), + }, { key: 'profiling', href: router.link('/services/:serviceName/profiling', { diff --git a/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx new file mode 100644 index 000000000000..71aeb54d4370 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function AggregatedTransactionsCallout() { + return ( + + {i18n.translate('xpack.apm.aggregatedTransactions.callout.title', { + defaultMessage: `This page is using transaction event data as no metrics events were found in the current time range.`, + })} + + } + iconType="iInCircle" + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index 4abd36a27731..4c593e80df0c 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -26,7 +26,7 @@ const transactionDurationLabel = i18n.translate( ); const transactionErrorRateLabel = i18n.translate( 'xpack.apm.home.alertsMenu.transactionErrorRate', - { defaultMessage: 'Transaction error rate' } + { defaultMessage: 'Failed transaction rate' } ); const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { defaultMessage: 'Error count', @@ -146,7 +146,7 @@ export function AlertingPopoverAndFlyout({ ], }, - // transaction error rate panel + // Failed transactions panel { id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, title: transactionErrorRateLabel, diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx index 069631b9f17a..caae47184510 100644 --- a/x-pack/plugins/apm/public/components/shared/backend_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -18,9 +18,10 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface BackendLinkProps { backendName: string; - query?: TypeOf['query']; + query: TypeOf['query']; subtype?: string; type?: string; + onClick?: React.ComponentProps['onClick']; } export function BackendLink({ @@ -28,6 +29,7 @@ export function BackendLink({ query, subtype, type, + onClick, }: BackendLinkProps) { const { link } = useApmRouter(); @@ -37,6 +39,7 @@ export function BackendLink({ path: { backendName }, query, })} + onClick={onClick} > diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index 81c4af44c90a..0f09b042a587 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -8,15 +8,19 @@ import { ALERT_DURATION, ALERT_EVALUATION_THRESHOLD, + ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_PRODUCER, - ALERT_OWNER, + ALERT_RULE_PRODUCER, + ALERT_RULE_CONSUMER, ALERT_SEVERITY_LEVEL, ALERT_START, ALERT_STATUS, ALERT_UUID, SPACE_IDS, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, } from '@kbn/rule-data-utils'; import { ValuesType } from 'utility-types'; import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; @@ -34,20 +38,19 @@ const theme = ({ eui: { euiColorDanger, euiColorWarning }, } as unknown) as EuiTheme; const alert: Alert = { - [SPACE_IDS]: ['space-id'], - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2057657.39], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [62879000], [ALERT_STATUS]: ['open'], [SPACE_IDS]: ['myfakespaceid'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - [ALERT_PRODUCER]: ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478180'], - [ALERT_OWNER]: ['apm'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_CONSUMER]: ['apm'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T16:16:05.183Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -55,7 +58,7 @@ const alert: Alert = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-01T16:15:02.304Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }; const chartStartTime = new Date(alert[ALERT_START]![0] as string).getTime(); const getFormatter: ObservabilityRuleTypeRegistry['getFormatter'] = () => () => ({ @@ -135,7 +138,7 @@ describe('getAlertAnnotations', () => { setSelectedAlertId, theme, })![0].props.dataValues[0].details - ).toEqual(alert['rule.name']![0]); + ).toEqual(alert[ALERT_RULE_NAME]![0]); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index fa0725018f78..f51494b8fa1d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -12,14 +12,23 @@ import { } from '@elastic/charts'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_UUID as ALERT_UUID_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_START, - ALERT_UUID, - RULE_ID, - RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_UUID as ALERT_UUID_NON_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import React, { Dispatch, SetStateAction } from 'react'; import { EuiTheme } from 'src/plugins/kibana_react/common'; import { ValuesType } from 'utility-types'; @@ -28,6 +37,13 @@ import { parseTechnicalFields } from '../../../../../../rule_registry/common'; import { asDuration, asPercent } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + type Alert = ValuesType< APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'] >; @@ -106,10 +122,10 @@ export function getAlertAnnotations({ const severityLevel = parsed[ALERT_SEVERITY_LEVEL]; const color = getAlertColor({ severityLevel, theme }); const header = getAlertHeader({ severityLevel }); - const formatter = getFormatter(parsed[RULE_ID]!); + const formatter = getFormatter(parsed[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsed[RULE_NAME], + reason: parsed[ALERT_RULE_NAME], ...(formatter?.({ fields: parsed, formatters: { asDuration, asPercent }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index 1a89f070bb5c..ff9e46fc0552 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -9,7 +9,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; @@ -27,6 +29,8 @@ import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header'; import * as urlHelpers from '../../../shared/Links/url_helpers'; import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; + interface Props { height?: number; } @@ -128,8 +132,10 @@ export function LatencyChart({ height }: Props) { anomalyTimeseries={anomalyTimeseries} alerts={alerts.filter( (alert) => - alert[RULE_ID]?.[0] === AlertType.TransactionDuration || - alert[RULE_ID]?.[0] === AlertType.TransactionDurationAnomaly + alert[ALERT_RULE_TYPE_ID]?.[0] === + AlertType.TransactionDuration || + alert[ALERT_RULE_TYPE_ID]?.[0] === + AlertType.TransactionDurationAnomaly )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index 71d517ad5387..39b7f488d68e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -8,12 +8,17 @@ import { ALERT_DURATION, ALERT_EVALUATION_THRESHOLD, + ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, ALERT_SEVERITY_LEVEL, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, + ALERT_RULE_PRODUCER, } from '@kbn/rule-data-utils'; import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; @@ -120,17 +125,17 @@ Example.args = { alertsResponse: { alerts: [ { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [10000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478180'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -138,21 +143,21 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T04:00:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [10000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_SEVERITY_LEVEL]: ['warning'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478181'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -160,21 +165,21 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T10:45:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [1000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_SEVERITY_LEVEL]: ['critical'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478182'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -182,7 +187,7 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T16:50:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, ], }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 18c765c50fbf..1685051bb44e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; +import { ALERT_RULE_TYPE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -114,7 +114,7 @@ export function TransactionErrorRateChart({ type: 'linemark', color: theme.eui.euiColorVis7, title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { - defaultMessage: 'Error rate (avg.)', + defaultMessage: 'Failed transaction rate (avg.)', }), }, ...(comparisonEnabled @@ -137,7 +137,7 @@ export function TransactionErrorRateChart({

{i18n.translate('xpack.apm.errorRate', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', })}

@@ -151,7 +151,8 @@ export function TransactionErrorRateChart({ yDomain={{ min: 0, max: 1 }} customTheme={comparisonChartThem} alerts={alerts.filter( - (alert) => alert[RULE_ID]?.[0] === AlertType.TransactionErrorRate + (alert) => + alert[ALERT_RULE_TYPE_ID]?.[0] === AlertType.TransactionErrorRate )} />
diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index a599a3bd0aa6..0b8fb787c8d8 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -113,7 +113,7 @@ export function DependenciesTable(props: Props) { { field: 'errorRateValue', name: i18n.translate('xpack.apm.dependenciesTable.columnErrorRate', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', }), width: `${unit * 10}px`, render: (_, { currentStats, previousStats }) => { diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx index d61f55fe53cf..a09ce958fdca 100644 --- a/x-pack/plugins/apm/public/components/shared/service_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -19,7 +19,7 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface ServiceLinkProps { agentName?: AgentName; - query?: TypeOf['query']; + query: TypeOf['query']; serviceName: string; } @@ -32,6 +32,7 @@ export function ServiceLink({ return ( { diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index d9268c14aa2e..3b649d77172e 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -27,13 +27,14 @@ const ContentWrapper = euiStyled.div` interface Props { text: string; content?: React.ReactNode; + 'data-test-subj'?: string; } export function TruncateWithTooltip(props: Props) { - const { text, content } = props; + const { text, content, ...rest } = props; return ( - + ; + export function getScreenSizes(windowWidth: number) { return { isXSmall: isWithinMaxBreakpoint(windowWidth, 'xs'), diff --git a/x-pack/plugins/apm/public/hooks/use_comparison.ts b/x-pack/plugins/apm/public/hooks/use_comparison.ts index b84942657dc9..2875d973a5e1 100644 --- a/x-pack/plugins/apm/public/hooks/use_comparison.ts +++ b/x-pack/plugins/apm/public/hooks/use_comparison.ts @@ -10,6 +10,7 @@ import { getTimeRangeComparison, } from '../components/shared/time_comparison/get_time_range_comparison'; import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmParams } from './use_apm_params'; import { useTheme } from './use_theme'; import { useTimeRange } from './use_time_range'; @@ -17,7 +18,16 @@ export function useComparison() { const theme = useTheme(); const comparisonChartTheme = getComparisonChartTheme(theme); - const { start, end } = useTimeRange(); + const { query } = useApmParams('/*'); + + if (!('rangeFrom' in query && 'rangeTo' in query)) { + throw new Error('rangeFrom or rangeTo not defined in query'); + } + + const { start, end } = useTimeRange({ + rangeFrom: query.rangeFrom, + rangeTo: query.rangeTo, + }); const { urlParams: { comparisonType, comparisonEnabled }, diff --git a/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx new file mode 100644 index 000000000000..51e5429633ea --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useFetcher } from './use_fetcher'; + +export function useFallbackToTransactionsFetcher() { + const { + urlParams: { kuery, start, end }, + } = useUrlParams(); + const { data = { fallbackToTransactions: false } } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/fallback_to_transactions', + params: { + query: { kuery, start, end }, + }, + }); + }, + [kuery, start, end] + ); + + return data; +} diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.test.ts b/x-pack/plugins/apm/public/hooks/use_time_range.test.ts new file mode 100644 index 000000000000..dbdd7de17165 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_time_range.test.ts @@ -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 { + act, + renderHook, + RenderHookResult, +} from '@testing-library/react-hooks'; +import { useTimeRange } from './use_time_range'; + +describe('useTimeRange', () => { + let hook: RenderHookResult< + Parameters[0], + ReturnType + >; + + beforeEach(() => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 12)).valueOf()); + + hook = renderHook( + (props) => { + const { rangeFrom, rangeTo } = props; + return useTimeRange({ rangeFrom, rangeTo }); + }, + { initialProps: { rangeFrom: 'now-15m', rangeTo: 'now' } } + ); + }); + + afterEach(() => {}); + + it('returns the parsed range on first render', () => { + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + }); + + it('only changes the parsed range when rangeFrom/rangeTo change', () => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); + + hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + + hook.rerender({ rangeFrom: 'now-30m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T12:30:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); + }); + + it('updates when refreshTimeRange is called', async () => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); + + hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + + act(() => { + hook.result.current.refreshTimeRange(); + }); + + expect(hook.result.current.start).toEqual('2021-01-01T12:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); + }); +}); diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.ts b/x-pack/plugins/apm/public/hooks/use_time_range.ts index 7afdfb7db6a0..8263767a402d 100644 --- a/x-pack/plugins/apm/public/hooks/use_time_range.ts +++ b/x-pack/plugins/apm/public/hooks/use_time_range.ts @@ -5,19 +5,46 @@ * 2.0. */ -import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { isEqual } from 'lodash'; +import { useCallback, useRef, useState } from 'react'; +import { getDateRange } from '../context/url_params_context/helpers'; -export function useTimeRange() { - const { - urlParams: { start, end }, - } = useUrlParams(); +export function useTimeRange({ + rangeFrom, + rangeTo, +}: { + rangeFrom: string; + rangeTo: string; +}) { + const rangeRef = useRef({ rangeFrom, rangeTo }); + + const [timeRangeId, setTimeRangeId] = useState(0); + + const stateRef = useRef(getDateRange({ state: {}, rangeFrom, rangeTo })); + + const updateParsedTime = useCallback(() => { + stateRef.current = getDateRange({ state: {}, rangeFrom, rangeTo }); + }, [rangeFrom, rangeTo]); + + if (!isEqual(rangeRef.current, { rangeFrom, rangeTo })) { + updateParsedTime(); + } + + const { start, end } = stateRef.current; + + const refreshTimeRange = useCallback(() => { + updateParsedTime(); + setTimeRangeId((id) => id + 1); + }, [setTimeRangeId, updateParsedTime]); if (!start || !end) { - throw new Error('Time range not set'); + throw new Error('start and/or end were unexpectedly not set'); } return { start, end, + refreshTimeRange, + timeRangeId, }; } diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 8175f46715b8..0631dba7e2a3 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; +import { UsageCollectionStart } from 'src/plugins/usage_collection/public'; import type { ConfigSchema } from '.'; import { AppMountParameters, @@ -32,9 +33,10 @@ import type { FeaturesPluginSetup } from '../../features/public'; import type { LicensingPluginSetup } from '../../licensing/public'; import type { MapsStartApi } from '../../maps/public'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; -import type { +import { FetchDataParams, HasDataParams, + METRIC_TYPE, ObservabilityPublicSetup, ObservabilityPublicStart, } from '../../observability/public'; @@ -112,7 +114,7 @@ export class ApmPlugin implements Plugin { // register observability nav if user has access to plugin plugins.observability.navigation.registerSections( from(core.getStartServices()).pipe( - map(([coreStart]) => { + map(([coreStart, pluginsStart]) => { if (coreStart.application.capabilities.apm.show) { return [ // APM navigation @@ -123,7 +125,24 @@ export class ApmPlugin implements Plugin { { label: servicesTitle, app: 'apm', path: '/services' }, { label: tracesTitle, app: 'apm', path: '/traces' }, { label: serviceMapTitle, app: 'apm', path: '/service-map' }, - { label: backendsTitle, app: 'apm', path: '/backends' }, + { + label: backendsTitle, + app: 'apm', + path: '/backends', + onClick: () => { + const { usageCollection } = pluginsStart as { + usageCollection?: UsageCollectionStart; + }; + + if (usageCollection) { + usageCollection.reportUiCounter( + 'apm', + METRIC_TYPE.CLICK, + 'side_nav_backend' + ); + } + }, + }, ], }, diff --git a/x-pack/plugins/apm/scripts/kibana-security/call_kibana.ts b/x-pack/plugins/apm/scripts/kibana-security/call_kibana.ts new file mode 100644 index 000000000000..60808cf0eb92 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/call_kibana.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import axios, { AxiosRequestConfig, AxiosError } from 'axios'; +import { once } from 'lodash'; +import { Elasticsearch } from './create_kibana_user_role'; + +export async function callKibana({ + elasticsearch, + kibanaHostname, + options, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + options: AxiosRequestConfig; +}): Promise { + const kibanaBasePath = await getKibanaBasePath({ kibanaHostname }); + const { username, password } = elasticsearch; + + const { data } = await axios.request({ + ...options, + baseURL: kibanaHostname + kibanaBasePath, + auth: { username, password }, + headers: { 'kbn-xsrf': 'true', ...options.headers }, + }); + return data; +} + +const getKibanaBasePath = once( + async ({ kibanaHostname }: { kibanaHostname: string }) => { + try { + await axios.request({ url: kibanaHostname, maxRedirects: 0 }); + } catch (e) { + if (isAxiosError(e)) { + const location = e.response?.headers?.location; + const isBasePath = RegExp(/^\/\w{3}$/).test(location); + return isBasePath ? location : ''; + } + + throw e; + } + return ''; + } +); + +export function isAxiosError(e: AxiosError | Error): e is AxiosError { + return 'isAxiosError' in e; +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts new file mode 100644 index 000000000000..d4814e05029a --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable no-console */ + +import { Role } from '../../../../security/common/model'; +import { callKibana, isAxiosError } from '../call_kibana'; +import { Elasticsearch } from '../create_kibana_user_role'; + +type Privilege = [] | ['read'] | ['all']; +export interface KibanaPrivileges { + base?: Privilege; + feature?: Record; +} + +export type RoleType = Omit; + +export async function createRole({ + elasticsearch, + kibanaHostname, + roleName, + role, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + roleName: string; + role: RoleType; +}) { + const roleFound = await getRole({ + elasticsearch, + kibanaHostname, + roleName, + }); + if (roleFound) { + console.log(`Skipping: Role "${roleName}" already exists`); + return Promise.resolve(); + } + + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'PUT', + url: `/api/security/role/${roleName}`, + data: { + metadata: { version: 1 }, + ...role, + }, + }, + }); + + console.log( + `Created role "${roleName}" with privilege "${JSON.stringify(role.kibana)}"` + ); +} + +async function getRole({ + elasticsearch, + kibanaHostname, + roleName, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + roleName: string; +}): Promise { + try { + return await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'GET', + url: `/api/security/role/${roleName}`, + }, + }); + } catch (e) { + // return empty if role doesn't exist + if (isAxiosError(e) && e.response?.status === 404) { + return null; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts new file mode 100644 index 000000000000..fea09c738360 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts @@ -0,0 +1,188 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import { difference, union } from 'lodash'; +import { callKibana, isAxiosError } from '../call_kibana'; +import { Elasticsearch, Kibana } from '../create_kibana_user_role'; +import { createRole } from './create_role'; +import { powerUserRole } from './power_user_role'; +import { readOnlyUserRole } from './read_only_user_role'; + +export async function createAPMUsers({ + kibana: { roleSuffix, hostname }, + elasticsearch, +}: { + kibana: Kibana; + elasticsearch: Elasticsearch; +}) { + const KIBANA_READ_ROLE = `kibana_read_${roleSuffix}`; + const KIBANA_POWER_ROLE = `kibana_power_${roleSuffix}`; + const APM_USER_ROLE = 'apm_user'; + + // roles definition + const roles = [ + { + roleName: KIBANA_READ_ROLE, + role: readOnlyUserRole, + }, + { + roleName: KIBANA_POWER_ROLE, + role: powerUserRole, + }, + ]; + + // create roles + await Promise.all( + roles.map(async (role) => + createRole({ elasticsearch, kibanaHostname: hostname, ...role }) + ) + ); + + // users definition + const users = [ + { + username: 'apm_read_user', + roles: [APM_USER_ROLE, KIBANA_READ_ROLE], + }, + { + username: 'apm_power_user', + roles: [APM_USER_ROLE, KIBANA_POWER_ROLE], + }, + ]; + + // create users + await Promise.all( + users.map(async (user) => + createOrUpdateUser({ elasticsearch, kibanaHostname: hostname, user }) + ) + ); +} + +interface User { + username: string; + roles: string[]; + full_name?: string; + email?: string; + enabled?: boolean; +} + +async function createOrUpdateUser({ + elasticsearch, + kibanaHostname, + user, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + user: User; +}) { + const existingUser = await getUser({ + elasticsearch, + kibanaHostname, + username: user.username, + }); + if (!existingUser) { + return createUser({ elasticsearch, kibanaHostname, newUser: user }); + } + + return updateUser({ + elasticsearch, + kibanaHostname, + existingUser, + newUser: user, + }); +} + +async function createUser({ + elasticsearch, + kibanaHostname, + newUser, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + newUser: User; +}) { + const user = await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'POST', + url: `/internal/security/users/${newUser.username}`, + data: { + ...newUser, + enabled: true, + password: elasticsearch.password, + }, + }, + }); + + console.log(`User "${newUser.username}" was created`); + return user; +} + +async function updateUser({ + elasticsearch, + kibanaHostname, + existingUser, + newUser, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + existingUser: User; + newUser: User; +}) { + const { username } = newUser; + const allRoles = union(existingUser.roles, newUser.roles); + const hasAllRoles = difference(allRoles, existingUser.roles).length === 0; + if (hasAllRoles) { + console.log( + `Skipping: User "${username}" already has neccesarry roles: "${newUser.roles}"` + ); + return; + } + + // assign role to user + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'POST', + url: `/internal/security/users/${username}`, + data: { ...existingUser, roles: allRoles }, + }, + }); + + console.log(`User "${username}" was updated`); +} + +async function getUser({ + elasticsearch, + kibanaHostname, + username, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + username: string; +}) { + try { + return await callKibana({ + elasticsearch, + kibanaHostname, + options: { + url: `/internal/security/users/${username}`, + }, + }); + } catch (e) { + // return empty if user doesn't exist + if (isAxiosError(e) && e.response?.status === 404) { + return null; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts new file mode 100644 index 000000000000..e9d10509f7fc --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RoleType } from './create_role'; + +export const powerUserRole: RoleType = { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + // core + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + ml: ['all'], + maps: ['all'], + graph: ['all'], + visualize: ['all'], + + // observability + logs: ['all'], + infrastructure: ['all'], + apm: ['all'], + uptime: ['all'], + + // security + siem: ['all'], + + // management + dev_tools: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + savedObjectsManagement: ['all'], + stackAlerts: ['all'], + fleet: ['all'], + actions: ['all'], + }, + spaces: ['*'], + }, + ], +}; diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts new file mode 100644 index 000000000000..794531da73a5 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RoleType } from './create_role'; + +export const readOnlyUserRole: RoleType = { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + // core + discover: ['read'], + dashboard: ['read'], + canvas: ['read'], + ml: ['read'], + maps: ['read'], + graph: ['read'], + visualize: ['read'], + + // observability + logs: ['read'], + infrastructure: ['read'], + apm: ['read'], + uptime: ['read'], + + // security + siem: ['read'], + + // management + dev_tools: ['read'], + advancedSettings: ['read'], + indexPatterns: ['read'], + savedObjectsManagement: ['read'], + stackAlerts: ['read'], + fleet: ['read'], + actions: ['read'], + }, + spaces: ['*'], + }, + ], +}; diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts new file mode 100644 index 000000000000..9520df8133bb --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts @@ -0,0 +1,112 @@ +/* + * 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 { callKibana, isAxiosError } from './call_kibana'; +import { createAPMUsers } from './create_apm_users'; + +/* eslint-disable no-console */ + +export interface Elasticsearch { + username: string; + password: string; +} + +export interface Kibana { + roleSuffix: string; + hostname: string; +} + +export async function createKibanaUserRole({ + kibana, + elasticsearch, +}: { + kibana: Kibana; + elasticsearch: Elasticsearch; +}) { + const version = await getKibanaVersion({ + elasticsearch, + kibanaHostname: kibana.hostname, + }); + console.log(`Connected to Kibana ${version}`); + + const isSecurityEnabled = await getIsSecurityEnabled({ + elasticsearch, + kibanaHostname: kibana.hostname, + }); + if (!isSecurityEnabled) { + throw new AbortError('Security must be enabled!'); + } + + await createAPMUsers({ kibana, elasticsearch }); +} + +async function getIsSecurityEnabled({ + elasticsearch, + kibanaHostname, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; +}) { + try { + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + url: `/internal/security/me`, + }, + }); + return true; + } catch (err) { + return false; + } +} + +async function getKibanaVersion({ + elasticsearch, + kibanaHostname, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; +}) { + try { + const res: { version: { number: number } } = await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'GET', + url: `/api/status`, + }, + }); + return res.version.number; + } catch (e) { + if (isAxiosError(e)) { + switch (e.response?.status) { + case 401: + throw new AbortError( + `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` + ); + + case 404: + throw new AbortError( + `Could not get version on ${e.config.url} (Code: 404)` + ); + + default: + throw new AbortError( + `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url "` + ); + } + } + throw e; + } +} + +export class AbortError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 81d5fe50e0ad..a0264f521137 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -7,46 +7,59 @@ /* eslint-disable no-console */ -import axios, { AxiosRequestConfig, AxiosError } from 'axios'; -import { union, difference, once } from 'lodash'; import { argv } from 'yargs'; +import { isAxiosError } from './call_kibana'; +import { createKibanaUserRole, AbortError } from './create_kibana_user_role'; -const KIBANA_ROLE_SUFFIX = argv.roleSuffix as string | undefined; -const ELASTICSEARCH_USERNAME = (argv.username as string) || 'elastic'; -const ELASTICSEARCH_PASSWORD = argv.password as string | undefined; -const KIBANA_BASE_URL = argv.kibanaUrl as string | undefined; +const esUserName = (argv.username as string) || 'elastic'; +const esPassword = argv.password as string | undefined; +const kibanaBaseUrl = argv.kibanaUrl as string | undefined; +const kibanaRoleSuffix = argv.roleSuffix as string | undefined; -console.log({ - KIBANA_ROLE_SUFFIX, - ELASTICSEARCH_USERNAME, - ELASTICSEARCH_PASSWORD, - KIBANA_BASE_URL, -}); +if (!esPassword) { + throw new Error( + 'Please specify credentials for elasticsearch: `--username elastic --password abcd` ' + ); +} -interface User { - username: string; - roles: string[]; - full_name?: string; - email?: string; - enabled?: boolean; +if (!kibanaBaseUrl) { + throw new Error( + 'Please specify the url for Kibana: `--kibana-url http://localhost:5601` ' + ); } -const getKibanaBasePath = once(async () => { - try { - await axios.request({ url: KIBANA_BASE_URL, maxRedirects: 0 }); - } catch (e) { - if (isAxiosError(e)) { - const location = e.response?.headers?.location; - const isBasePath = RegExp(/^\/\w{3}$/).test(location); - return isBasePath ? location : ''; - } +if ( + !kibanaBaseUrl.startsWith('https://') && + !kibanaBaseUrl.startsWith('http://') +) { + throw new Error( + 'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`' + ); +} - throw e; - } - return ''; +if (!kibanaRoleSuffix) { + throw new Error( + 'Please specify a unique suffix that will be added to your roles with `--role-suffix ` ' + ); +} + +console.log({ + kibanaRoleSuffix, + esUserName, + esPassword, + kibanaBaseUrl, }); -init().catch((e) => { +createKibanaUserRole({ + kibana: { + roleSuffix: kibanaRoleSuffix, + hostname: kibanaBaseUrl, + }, + elasticsearch: { + username: esUserName, + password: esPassword, + }, +}).catch((e) => { if (e instanceof AbortError) { console.error(e.message); } else if (isAxiosError(e)) { @@ -69,324 +82,3 @@ init().catch((e) => { console.error(e); } }); - -async function init() { - if (!ELASTICSEARCH_PASSWORD) { - console.log( - 'Please specify credentials for elasticsearch: `--username elastic --password abcd` ' - ); - return; - } - - if (!KIBANA_BASE_URL) { - console.log( - 'Please specify the url for Kibana: `--kibana-url http://localhost:5601` ' - ); - return; - } - - if ( - !KIBANA_BASE_URL.startsWith('https://') && - !KIBANA_BASE_URL.startsWith('http://') - ) { - console.log( - 'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`' - ); - return; - } - - if (!KIBANA_ROLE_SUFFIX) { - console.log( - 'Please specify a unique suffix that will be added to your roles with `--role-suffix ` ' - ); - return; - } - - const version = await getKibanaVersion(); - console.log(`Connected to Kibana ${version}`); - - const isEnabled = await isSecurityEnabled(); - if (!isEnabled) { - console.log('Security must be enabled!'); - return; - } - - const APM_READ_ROLE = `apm_read_${KIBANA_ROLE_SUFFIX}`; - const KIBANA_READ_ROLE = `kibana_read_${KIBANA_ROLE_SUFFIX}`; - const KIBANA_WRITE_ROLE = `kibana_write_${KIBANA_ROLE_SUFFIX}`; - const APM_USER_ROLE = 'apm_user'; - - // create roles - await createRole({ - roleName: APM_READ_ROLE, - kibanaPrivileges: { feature: { apm: ['read'] } }, - }); - await createRole({ - roleName: KIBANA_READ_ROLE, - kibanaPrivileges: { - feature: { - // core - discover: ['read'], - dashboard: ['read'], - canvas: ['read'], - ml: ['read'], - maps: ['read'], - graph: ['read'], - visualize: ['read'], - - // observability - logs: ['read'], - infrastructure: ['read'], - apm: ['read'], - uptime: ['read'], - - // security - siem: ['read'], - - // management - dev_tools: ['read'], - advancedSettings: ['read'], - indexPatterns: ['read'], - savedObjectsManagement: ['read'], - stackAlerts: ['read'], - fleet: ['read'], - actions: ['read'], - }, - }, - }); - await createRole({ - roleName: KIBANA_WRITE_ROLE, - kibanaPrivileges: { - feature: { - // core - discover: ['all'], - dashboard: ['all'], - canvas: ['all'], - ml: ['all'], - maps: ['all'], - graph: ['all'], - visualize: ['all'], - - // observability - logs: ['all'], - infrastructure: ['all'], - apm: ['all'], - uptime: ['all'], - - // security - siem: ['all'], - - // management - dev_tools: ['all'], - advancedSettings: ['all'], - indexPatterns: ['all'], - savedObjectsManagement: ['all'], - stackAlerts: ['all'], - fleet: ['all'], - actions: ['all'], - }, - }, - }); - - // read access only to APM + apm index access - await createOrUpdateUser({ - username: 'apm_read_user', - roles: [APM_USER_ROLE, APM_READ_ROLE], - }); - - // read access to all apps + apm index access - await createOrUpdateUser({ - username: 'kibana_read_user', - roles: [APM_USER_ROLE, KIBANA_READ_ROLE], - }); - - // read/write access to all apps + apm index access - await createOrUpdateUser({ - username: 'kibana_write_user', - roles: [APM_USER_ROLE, KIBANA_WRITE_ROLE], - }); -} - -async function isSecurityEnabled() { - try { - await callKibana({ - url: `/internal/security/me`, - }); - return true; - } catch (err) { - return false; - } -} - -async function callKibana(options: AxiosRequestConfig): Promise { - const kibanaBasePath = await getKibanaBasePath(); - - if (!ELASTICSEARCH_PASSWORD) { - throw new Error('Missing `--password`'); - } - - const { data } = await axios.request({ - ...options, - baseURL: KIBANA_BASE_URL + kibanaBasePath, - auth: { - username: ELASTICSEARCH_USERNAME, - password: ELASTICSEARCH_PASSWORD, - }, - headers: { 'kbn-xsrf': 'true', ...options.headers }, - }); - return data; -} - -type Privilege = [] | ['read'] | ['all']; - -async function createRole({ - roleName, - kibanaPrivileges, -}: { - roleName: string; - kibanaPrivileges: { base?: Privilege; feature?: Record }; -}) { - const role = await getRole(roleName); - if (role) { - console.log(`Skipping: Role "${roleName}" already exists`); - return; - } - - await callKibana({ - method: 'PUT', - url: `/api/security/role/${roleName}`, - data: { - metadata: { version: 1 }, - elasticsearch: { cluster: [], indices: [] }, - kibana: [ - { - base: kibanaPrivileges.base ?? [], - feature: kibanaPrivileges.feature ?? {}, - spaces: ['*'], - }, - ], - }, - }); - - console.log( - `Created role "${roleName}" with privilege "${JSON.stringify( - kibanaPrivileges - )}"` - ); -} - -async function createOrUpdateUser(newUser: User) { - const existingUser = await getUser(newUser.username); - if (!existingUser) { - return createUser(newUser); - } - - return updateUser(existingUser, newUser); -} - -async function createUser(newUser: User) { - const user = await callKibana({ - method: 'POST', - url: `/internal/security/users/${newUser.username}`, - data: { - ...newUser, - enabled: true, - password: ELASTICSEARCH_PASSWORD, - }, - }); - - console.log(`User "${newUser.username}" was created`); - return user; -} - -async function updateUser(existingUser: User, newUser: User) { - const { username } = newUser; - const allRoles = union(existingUser.roles, newUser.roles); - const hasAllRoles = difference(allRoles, existingUser.roles).length === 0; - if (hasAllRoles) { - console.log( - `Skipping: User "${username}" already has neccesarry roles: "${newUser.roles}"` - ); - return; - } - - // assign role to user - await callKibana({ - method: 'POST', - url: `/internal/security/users/${username}`, - data: { ...existingUser, roles: allRoles }, - }); - - console.log(`User "${username}" was updated`); -} - -async function getUser(username: string) { - try { - return await callKibana({ - url: `/internal/security/users/${username}`, - }); - } catch (e) { - // return empty if user doesn't exist - if (isAxiosError(e) && e.response?.status === 404) { - return null; - } - - throw e; - } -} - -async function getRole(roleName: string) { - try { - return await callKibana({ - method: 'GET', - url: `/api/security/role/${roleName}`, - }); - } catch (e) { - // return empty if role doesn't exist - if (isAxiosError(e) && e.response?.status === 404) { - return null; - } - - throw e; - } -} - -async function getKibanaVersion() { - try { - const res: { version: { number: number } } = await callKibana({ - method: 'GET', - url: `/api/status`, - }); - return res.version.number; - } catch (e) { - if (isAxiosError(e)) { - switch (e.response?.status) { - case 401: - throw new AbortError( - `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` - ); - - case 404: - throw new AbortError( - `Could not get version on ${e.config.url} (Code: 404)` - ); - - default: - throw new AbortError( - `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url "` - ); - } - } - throw e; - } -} - -function isAxiosError(e: AxiosError | Error): e is AxiosError { - return 'isAxiosError' in e; -} - -class AbortError extends Error { - constructor(message: string) { - super(message); - } -} diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index 888c929a2c72..f4d8ffc2749c 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -17,7 +17,7 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; @@ -75,12 +75,9 @@ export async function getTransactionErrorRateChartPreview({ } return resp.aggregations.timeseries.buckets.map((bucket) => { - const errorPercentage = calculateTransactionErrorPercentage( - bucket.outcomes - ); return { x: bucket.key, - y: errorPercentage, + y: calculateFailedTransactionRate(bucket.outcomes), }; }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index bdd6b240c4bb..6a6a67e9fd97 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -7,10 +7,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED, @@ -34,6 +39,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts new file mode 100644 index 000000000000..ec0a986e2722 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; +import { createRuleTypeMocks } from './test_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +describe('registerTransactionDurationAlertType', () => { + it('sends alert when value is greater than threashold', async () => { + const { + services, + dependencies, + executor, + scheduleActions, + } = createRuleTypeMocks(); + + registerTransactionDurationAlertType(dependencies); + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 2, + }, + }, + aggregations: { + latency: { + value: 5500000, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + const params = { + threshold: 3000, + windowSize: 5, + windowUnit: 'm', + transactionType: 'request', + serviceName: 'opbeans-java', + }; + await executor({ params }); + expect(scheduleActions).toHaveBeenCalledTimes(1); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + transactionType: 'request', + serviceName: 'opbeans-java', + environment: 'Not defined', + threshold: 3000, + triggerValue: '5,500 ms', + interval: `5m`, + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index c14675cb9398..790e62eae66d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -8,10 +8,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { getEnvironmentLabel, @@ -36,6 +41,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ serviceName: schema.string(), transactionType: schema.string(), @@ -149,9 +157,10 @@ export function registerTransactionDurationAlertType({ const transactionDuration = 'values' in latency ? Object.values(latency.values)[0] : latency?.value; - const threshold = alertParams.threshold * 1000; + // Converts threshold to microseconds because this is the unit used on transactionDuration + const thresholdMicroseconds = alertParams.threshold * 1000; - if (transactionDuration && transactionDuration > threshold) { + if (transactionDuration && transactionDuration > thresholdMicroseconds) { const durationFormatter = getDurationFormatter(transactionDuration); const transactionDurationFormatted = durationFormatter( transactionDuration @@ -168,14 +177,14 @@ export function registerTransactionDurationAlertType({ [TRANSACTION_TYPE]: alertParams.transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: transactionDuration, - [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold * 1000, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { transactionType: alertParams.transactionType, serviceName: alertParams.serviceName, environment: getEnvironmentLabel(alertParams.environment), - threshold, + threshold: alertParams.threshold, triggerValue: transactionDurationFormatted, interval: `${alertParams.windowSize}${alertParams.windowUnit}`, }); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index de0657d075d7..2041f06a5a6a 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -9,12 +9,19 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; import { ESSearchResponse } from 'src/core/types/elasticsearch'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { getSeverity } from '../../../common/anomaly_detection'; @@ -39,6 +46,11 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY_VALUE: typeof ALERT_SEVERITY_VALUE_TYPED = ALERT_SEVERITY_VALUE_NON_TYPED; + const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), transactionType: schema.maybe(schema.string()), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index a39730265953..fc63e4827e5d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -7,10 +7,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, @@ -38,6 +43,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index 081c66dc2c47..8cfaaa6021b1 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -19,7 +19,7 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimeseriesAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; @@ -145,7 +145,7 @@ export async function getErrorRateTimeSeries({ return { ...topSig, - timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), + timeseries: getFailedTransactionRateTimeSeries(agg.timeseries.buckets), }; }), }; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts index f3477273806b..0c36a2853746 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts @@ -9,7 +9,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimeseriesAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; @@ -43,7 +43,7 @@ export async function getOverallErrorTimeseries(options: CorrelationsOptions) { return { overall: { - timeseries: getTransactionErrorRateTimeSeries( + timeseries: getFailedTransactionRateTimeSeries( aggregations.timeseries.buckets ), }, diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index b0065b70673a..428378178afc 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -6,7 +6,15 @@ */ import { APMPlugin, APMRouteHandlerResources } from '../..'; -import { ExternalCallback } from '../../../../fleet/server'; +import { + ExternalCallback, + PostPackagePolicyDeleteCallback, + PutPackagePolicyUpdateCallback, +} from '../../../../fleet/server'; +import { + NewPackagePolicy, + UpdatePackagePolicy, +} from '../../../../fleet/common'; import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types'; import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames'; import { APMPluginStartDependencies } from '../../types'; @@ -53,7 +61,7 @@ export async function registerFleetPolicyCallbacks({ } type ExternalCallbackParams = Parameters; -export type PackagePolicy = ExternalCallbackParams[0]; +export type PackagePolicy = NewPackagePolicy | UpdatePackagePolicy; type Context = ExternalCallbackParams[1]; type Request = ExternalCallbackParams[2]; @@ -66,13 +74,15 @@ function registerPackagePolicyExternalCallback({ logger, }: { fleetPluginStart: NonNullable; - callbackName: ExternalCallback[0]; + callbackName: 'packagePolicyCreate' | 'packagePolicyUpdate'; plugins: APMRouteHandlerResources['plugins']; ruleDataClient: APMRouteHandlerResources['ruleDataClient']; config: NonNullable; logger: NonNullable; }) { - const callbackFn: ExternalCallback[1] = async ( + const callbackFn: + | PostPackagePolicyDeleteCallback + | PutPackagePolicyUpdateCallback = async ( packagePolicy: PackagePolicy, context: Context, request: Request diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts new file mode 100644 index 000000000000..d5c7eb596a98 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSearchAggregatedTransactions } from '.'; +import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../setup_request'; + +export async function getFallbackToTransactions({ + setup: { config, start, end, apmEventClient }, + kuery, +}: { + setup: Setup & Partial; + kuery?: string; +}): Promise { + const searchAggregatedTransactions = + config['xpack.apm.searchAggregatedTransactions']; + const neverSearchAggregatedTransactions = + searchAggregatedTransactions === SearchAggregatedTransactionSetting.never; + + if (neverSearchAggregatedTransactions) { + return false; + } + + const searchesAggregatedTransactions = await getSearchAggregatedTransactions({ + config, + start, + end, + apmEventClient, + kuery, + }); + return !searchesAggregatedTransactions; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts b/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts index d09f5d9d3f11..1b9e1b56830a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts +++ b/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts @@ -15,3 +15,8 @@ export function calculateThroughput({ const durationAsMinutes = (end - start) / 1000 / 60; return value / durationAsMinutes; } + +export type ThroughputUnit = 'minute' | 'second'; +export function getThroughputUnit(bucketSize: number): ThroughputUnit { + return bucketSize >= 60 ? 'minute' : 'second'; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index eb82a8981108..863929a2719a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -12,7 +12,7 @@ import { calculateAuto } from './calculate_auto'; export function getBucketSize({ start, end, - numBuckets = 100, + numBuckets = 50, minBucketSize, }: { start: number; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index a277b6d4e8c5..ba67a42fbbad 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -62,7 +62,10 @@ interface SetupRequestParams { type InferSetup = Setup & (TParams extends { query: { start: number } } ? { start: number } : {}) & - (TParams extends { query: { end: number } } ? { end: number } : {}); + (TParams extends { query: { end: number } } ? { end: number } : {}) & + (TParams extends { query: Partial } + ? Partial + : {}); export async function setupRequest({ context, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 41d9c373710c..240cbceb6c87 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -35,7 +35,7 @@ export const getTimeseriesAggregation = ( aggs: { outcomes: getOutcomeAggregation() }, }); -export function calculateTransactionErrorPercentage( +export function calculateFailedTransactionRate( outcomeResponse: AggregationResultOf ) { const outcomes = Object.fromEntries( @@ -48,7 +48,7 @@ export function calculateTransactionErrorPercentage( return failedTransactions / (successfulTransactions + failedTransactions); } -export function getTransactionErrorRateTimeSeries( +export function getFailedTransactionRateTimeSeries( buckets: AggregationResultOf< { date_histogram: AggregationOptionsByType['date_histogram']; @@ -60,7 +60,7 @@ export function getTransactionErrorRateTimeSeries( return buckets.map((dateBucket) => { return { x: dateBucket.key, - y: calculateTransactionErrorPercentage(dateBucket.outcomes), + y: calculateFailedTransactionRate(dateBucket.outcomes), }; }); } diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 70585f9a9bf2..28fab3369b1e 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { SERVICE_NAME, @@ -21,10 +20,7 @@ export async function hasRumData({ setup: Setup & Partial; }) { try { - const { - start = moment().subtract(24, 'h').valueOf(), - end = moment().valueOf(), - } = setup; + const { start, end } = setup; const params = { apm: { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index 90d24b6587f4..ae42a0c94fe9 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -5,28 +5,25 @@ * 2.0. */ -import { shuffle, range } from 'lodash'; +import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; -import { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; -import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; -import { fetchTransactionDurationPercentiles } from './query_percentiles'; -import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; -import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; import type { - AsyncSearchProviderProgress, SearchServiceParams, SearchServiceFetchParams, - SearchServiceValue, } from '../../../../common/search_strategies/correlations/types'; -import { computeExpectationsAndRanges } from './utils/aggregation_utils'; -import { fetchTransactionDurationFractions } from './query_fractions'; - -const CORRELATION_THRESHOLD = 0.3; -const KS_TEST_THRESHOLD = 0.1; - -const currentTimeAsString = () => new Date().toISOString(); +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { + fetchTransactionDurationFieldCandidates, + fetchTransactionDurationFieldValuePairs, + fetchTransactionDurationFractions, + fetchTransactionDurationPercentiles, + fetchTransactionDurationHistograms, + fetchTransactionDurationHistogramRangeSteps, + fetchTransactionDurationRanges, +} from './queries'; +import { computeExpectationsAndRanges } from './utils'; +import { asyncSearchServiceLogProvider } from './async_search_service_log'; +import { asyncSearchServiceStateProvider } from './async_search_service_state'; export const asyncSearchServiceProvider = ( esClient: ElasticsearchClient, @@ -34,40 +31,11 @@ export const asyncSearchServiceProvider = ( searchServiceParams: SearchServiceParams, includeFrozen: boolean ) => { - let isCancelled = false; - let isRunning = true; - let error: Error; - let ccsWarning = false; - const log: string[] = []; - const logMessage = (message: string) => - log.push(`${currentTimeAsString()}: ${message}`); - - const progress: AsyncSearchProviderProgress = { - started: Date.now(), - loadedHistogramStepsize: 0, - loadedOverallHistogram: 0, - loadedFieldCanditates: 0, - loadedFieldValuePairs: 0, - loadedHistograms: 0, - getOverallProgress: () => - progress.loadedHistogramStepsize * 0.025 + - progress.loadedOverallHistogram * 0.025 + - progress.loadedFieldCanditates * 0.025 + - progress.loadedFieldValuePairs * 0.025 + - progress.loadedHistograms * 0.9, - }; + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); - const values: SearchServiceValue[] = []; - let overallHistogram: HistogramItem[] | undefined; + const state = asyncSearchServiceStateProvider(); - let percentileThresholdValue: number; - - const cancel = () => { - logMessage(`Service cancelled.`); - isCancelled = true; - }; - - const fetchCorrelations = async () => { + async function fetchCorrelations() { let params: SearchServiceFetchParams | undefined; try { @@ -75,6 +43,7 @@ export const asyncSearchServiceProvider = ( params = { ...searchServiceParams, index: indices['apm_oss.transactionIndices'], + includeFrozen, }; // 95th percentile to be displayed as a marker in the log log chart @@ -86,24 +55,27 @@ export const asyncSearchServiceProvider = ( params, params.percentileThreshold ? [params.percentileThreshold] : undefined ); - percentileThresholdValue = + const percentileThresholdValue = percentileThreshold[`${params.percentileThreshold}.0`]; + state.setPercentileThresholdValue(percentileThresholdValue); - logMessage( + addLogMessage( `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` ); // finish early if we weren't able to identify the percentileThresholdValue. if (percentileThresholdValue === undefined) { - logMessage( + addLogMessage( `Abort service since percentileThresholdValue could not be determined.` ); - progress.loadedHistogramStepsize = 1; - progress.loadedOverallHistogram = 1; - progress.loadedFieldCanditates = 1; - progress.loadedFieldValuePairs = 1; - progress.loadedHistograms = 1; - isRunning = false; + state.setProgress({ + loadedHistogramStepsize: 1, + loadedOverallHistogram: 1, + loadedFieldCanditates: 1, + loadedFieldValuePairs: 1, + loadedHistograms: 1, + }); + state.setIsRunning(false); return; } @@ -111,12 +83,12 @@ export const asyncSearchServiceProvider = ( esClient, params ); - progress.loadedHistogramStepsize = 1; + state.setProgress({ loadedHistogramStepsize: 1 }); - logMessage(`Loaded histogram range steps.`); + addLogMessage(`Loaded histogram range steps.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -125,13 +97,13 @@ export const asyncSearchServiceProvider = ( params, histogramRangeSteps ); - progress.loadedOverallHistogram = 1; - overallHistogram = overallLogHistogramChartData; + state.setProgress({ loadedOverallHistogram: 1 }); + state.setOverallHistogram(overallLogHistogramChartData); - logMessage(`Loaded overall histogram chart data.`); + addLogMessage(`Loaded overall histogram chart data.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -142,10 +114,10 @@ export const asyncSearchServiceProvider = ( } = await fetchTransactionDurationPercentiles(esClient, params, percents); const percentiles = Object.values(percentilesRecords); - logMessage(`Loaded percentiles.`); + addLogMessage(`Loaded percentiles.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -154,21 +126,22 @@ export const asyncSearchServiceProvider = ( params ); - logMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); + addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - progress.loadedFieldCanditates = 1; + state.setProgress({ loadedFieldCanditates: 1 }); const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( esClient, params, fieldCandidates, - progress + state, + addLogMessage ); - logMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); + addLogMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -181,114 +154,75 @@ export const asyncSearchServiceProvider = ( totalDocCount, } = await fetchTransactionDurationFractions(esClient, params, ranges); - logMessage(`Loaded fractions and totalDocCount of ${totalDocCount}.`); - - async function* fetchTransactionDurationHistograms() { - for (const item of shuffle(fieldValuePairs)) { - if (params === undefined || item === undefined || isCancelled) { - isRunning = false; - return; - } - - // If one of the fields have an error - // We don't want to stop the whole process - try { - const { - correlation, - ksTest, - } = await fetchTransactionDurationCorrelation( - esClient, - params, - expectations, - ranges, - fractions, - totalDocCount, - item.field, - item.value - ); - - if (isCancelled) { - isRunning = false; - return; - } - - if ( - correlation !== null && - correlation > CORRELATION_THRESHOLD && - ksTest !== null && - ksTest < KS_TEST_THRESHOLD - ) { - const logHistogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - item.field, - item.value - ); - yield { - ...item, - correlation, - ksTest, - histogram: logHistogram, - }; - } else { - yield undefined; - } - } catch (e) { - // don't fail the whole process for individual correlation queries, - // just add the error to the internal log and check if we'd want to set the - // cross-cluster search compatibility warning to true. - logMessage( - `Failed to fetch correlation/kstest for '${item.field}/${item.value}'` - ); - if (params?.index.includes(':')) { - ccsWarning = true; - } - yield undefined; - } - } - } + addLogMessage(`Loaded fractions and totalDocCount of ${totalDocCount}.`); let loadedHistograms = 0; - for await (const item of fetchTransactionDurationHistograms()) { + for await (const item of fetchTransactionDurationHistograms( + esClient, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { if (item !== undefined) { - values.push(item); + state.addValue(item); } loadedHistograms++; - progress.loadedHistograms = loadedHistograms / fieldValuePairs.length; + state.setProgress({ + loadedHistograms: loadedHistograms / fieldValuePairs.length, + }); } - logMessage( - `Identified ${values.length} significant correlations out of ${fieldValuePairs.length} field/value pairs.` + addLogMessage( + `Identified ${ + state.getState().values.length + } significant correlations out of ${ + fieldValuePairs.length + } field/value pairs.` ); } catch (e) { - error = e; + state.setError(e); } - if (error !== undefined && params?.index.includes(':')) { - ccsWarning = true; + if (state.getState().error !== undefined && params?.index.includes(':')) { + state.setCcsWarning(true); } - isRunning = false; - }; + state.setIsRunning(false); + } fetchCorrelations(); return () => { - const sortedValues = values.sort((a, b) => b.correlation - a.correlation); + const { + ccsWarning, + error, + isRunning, + overallHistogram, + percentileThresholdValue, + progress, + } = state.getState(); return { ccsWarning, error, - log, + log: getLogMessages(), isRunning, - loaded: Math.round(progress.getOverallProgress() * 100), + loaded: Math.round(state.getOverallProgress() * 100), overallHistogram, started: progress.started, total: 100, - values: sortedValues, + values: state.getValuesSortedByCorrelation(), percentileThresholdValue, - cancel, + cancel: () => { + addLogMessage(`Service cancelled.`); + state.setIsCancelled(true); + }, }; }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts new file mode 100644 index 000000000000..bbeb8435e61b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { asyncSearchServiceLogProvider } from './async_search_service_log'; + +describe('async search service', () => { + describe('asyncSearchServiceLogProvider', () => { + it('adds and retrieves messages from the log', async () => { + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + + addLogMessage('the first message'); + addLogMessage('the second message'); + + expect(getLogMessages()).toEqual([ + '2014-02-12T11:00:00.000Z: the first message', + '2014-02-12T11:00:00.000Z: the second message', + ]); + + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts new file mode 100644 index 000000000000..e69d2f55b6c5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { currentTimeAsString } from './utils'; + +interface LogMessage { + timestamp: string; + message: string; + error?: string; +} + +export const asyncSearchServiceLogProvider = () => { + const log: LogMessage[] = []; + + function addLogMessage(message: string, error?: string) { + log.push({ + timestamp: currentTimeAsString(), + message, + ...(error !== undefined ? { error } : {}), + }); + } + + function getLogMessages() { + return log.map((l) => `${l.timestamp}: ${l.message}`); + } + + return { addLogMessage, getLogMessages }; +}; + +export type AsyncSearchServiceLog = ReturnType< + typeof asyncSearchServiceLogProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts new file mode 100644 index 000000000000..cfa1bf2a5ad7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { asyncSearchServiceStateProvider } from './async_search_service_state'; + +describe('async search service', () => { + describe('asyncSearchServiceStateProvider', () => { + it('initializes with default state', () => { + const state = asyncSearchServiceStateProvider(); + const defaultState = state.getState(); + const defaultProgress = state.getOverallProgress(); + + expect(defaultState.ccsWarning).toBe(false); + expect(defaultState.error).toBe(undefined); + expect(defaultState.isCancelled).toBe(false); + expect(defaultState.isRunning).toBe(true); + expect(defaultState.overallHistogram).toBe(undefined); + expect(defaultState.progress.loadedFieldCanditates).toBe(0); + expect(defaultState.progress.loadedFieldValuePairs).toBe(0); + expect(defaultState.progress.loadedHistogramStepsize).toBe(0); + expect(defaultState.progress.loadedHistograms).toBe(0); + expect(defaultState.progress.loadedOverallHistogram).toBe(0); + expect(defaultState.progress.started > 0).toBe(true); + + expect(defaultProgress).toBe(0); + }); + + it('returns updated state', () => { + const state = asyncSearchServiceStateProvider(); + + state.setCcsWarning(true); + state.setError(new Error('the-error-message')); + state.setIsCancelled(true); + state.setIsRunning(false); + state.setOverallHistogram([{ key: 1392202800000, doc_count: 1234 }]); + state.setProgress({ loadedHistograms: 0.5 }); + + const updatedState = state.getState(); + const updatedProgress = state.getOverallProgress(); + + expect(updatedState.ccsWarning).toBe(true); + expect(updatedState.error?.message).toBe('the-error-message'); + expect(updatedState.isCancelled).toBe(true); + expect(updatedState.isRunning).toBe(false); + expect(updatedState.overallHistogram).toEqual([ + { key: 1392202800000, doc_count: 1234 }, + ]); + expect(updatedState.progress.loadedFieldCanditates).toBe(0); + expect(updatedState.progress.loadedFieldValuePairs).toBe(0); + expect(updatedState.progress.loadedHistogramStepsize).toBe(0); + expect(updatedState.progress.loadedHistograms).toBe(0.5); + expect(updatedState.progress.loadedOverallHistogram).toBe(0); + expect(updatedState.progress.started > 0).toBe(true); + + expect(updatedProgress).toBe(0.45); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts new file mode 100644 index 000000000000..d0aac8987e07 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + AsyncSearchProviderProgress, + SearchServiceValue, +} from '../../../../common/search_strategies/correlations/types'; + +import { HistogramItem } from './queries'; + +export const asyncSearchServiceStateProvider = () => { + let ccsWarning = false; + function setCcsWarning(d: boolean) { + ccsWarning = d; + } + + let error: Error; + function setError(d: Error) { + error = d; + } + + let isCancelled = false; + function getIsCancelled() { + return isCancelled; + } + function setIsCancelled(d: boolean) { + isCancelled = d; + } + + let isRunning = true; + function setIsRunning(d: boolean) { + isRunning = d; + } + + let overallHistogram: HistogramItem[] | undefined; + function setOverallHistogram(d: HistogramItem[]) { + overallHistogram = d; + } + + let percentileThresholdValue: number; + function setPercentileThresholdValue(d: number) { + percentileThresholdValue = d; + } + + let progress: AsyncSearchProviderProgress = { + started: Date.now(), + loadedHistogramStepsize: 0, + loadedOverallHistogram: 0, + loadedFieldCanditates: 0, + loadedFieldValuePairs: 0, + loadedHistograms: 0, + }; + function getOverallProgress() { + return ( + progress.loadedHistogramStepsize * 0.025 + + progress.loadedOverallHistogram * 0.025 + + progress.loadedFieldCanditates * 0.025 + + progress.loadedFieldValuePairs * 0.025 + + progress.loadedHistograms * 0.9 + ); + } + function setProgress( + d: Partial> + ) { + progress = { + ...progress, + ...d, + }; + } + + const values: SearchServiceValue[] = []; + function addValue(d: SearchServiceValue) { + values.push(d); + } + + function getValuesSortedByCorrelation() { + return values.sort((a, b) => b.correlation - a.correlation); + } + + function getState() { + return { + ccsWarning, + error, + isCancelled, + isRunning, + overallHistogram, + percentileThresholdValue, + progress, + values, + }; + } + + return { + addValue, + getIsCancelled, + getOverallProgress, + getState, + getValuesSortedByCorrelation, + setCcsWarning, + setError, + setIsCancelled, + setIsRunning, + setOverallHistogram, + setPercentileThresholdValue, + setProgress, + }; +}; + +export type AsyncSearchServiceState = ReturnType< + typeof asyncSearchServiceStateProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts index 5420479bfffb..6b96b6b9d213 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts @@ -76,3 +76,6 @@ export const PERCENTILES_STEP = 2; export const TERMS_SIZE = 20; export const SIGNIFICANT_FRACTION = 3; export const SIGNIFICANT_VALUE_DIGITS = 3; + +export const CORRELATION_THRESHOLD = 0.3; +export const KS_TEST_THRESHOLD = 0.1; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts new file mode 100644 index 000000000000..dc11b4860a8b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; + +describe('correlations', () => { + describe('getPrioritizedFieldValuePairs', () => { + it('returns fields without prioritization in the same order', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'the-field-2', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'the-field-1', + 'the-field-2', + ]); + }); + + it('returns fields with already sorted prioritization in the same order', () => { + const fieldValuePairs = [ + { field: 'service.version', value: 'the-value-1' }, + { field: 'the-field-2', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'the-field-2', + ]); + }); + + it('returns fields with unsorted prioritization in the corrected order', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'service.version', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'the-field-1', + ]); + }); + + it('considers prefixes when sorting', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'service.version', value: 'the-value-2' }, + { field: 'cloud.the-field-3', value: 'the-value-3' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'cloud.the-field-3', + 'the-field-1', + ]); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts new file mode 100644 index 000000000000..ddfd87c83f9f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts @@ -0,0 +1,33 @@ +/* + * 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 { FIELDS_TO_ADD_AS_CANDIDATE } from '../constants'; +import { hasPrefixToInclude } from '../utils'; + +import type { FieldValuePairs } from './query_field_value_pairs'; + +export const getPrioritizedFieldValuePairs = ( + fieldValuePairs: FieldValuePairs +) => { + const prioritizedFields = [...FIELDS_TO_ADD_AS_CANDIDATE]; + + return fieldValuePairs.sort((a, b) => { + const hasPrefixA = hasPrefixToInclude(a.field); + const hasPrefixB = hasPrefixToInclude(b.field); + + const includesA = prioritizedFields.includes(a.field); + const includesB = prioritizedFields.includes(b.field); + + if ((includesA || hasPrefixA) && !includesB && !hasPrefixB) { + return -1; + } else if (!includesA && !hasPrefixA && (includesB || hasPrefixB)) { + return 1; + } + + return 0; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts similarity index 90% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index 016355b3a641..c27757b4fae6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -11,7 +11,12 @@ describe('correlations', () => { describe('getQueryWithParams', () => { it('returns the most basic query filtering on processor.event=transaction', () => { const query = getQueryWithParams({ - params: { index: 'apm-*', start: '2020', end: '2021' }, + params: { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, + }, }); expect(query).toEqual({ bool: { @@ -41,6 +46,7 @@ describe('correlations', () => { end: '2021', environment: 'dev', percentileThresholdValue: 75, + includeFrozen: false, }, }); expect(query).toEqual({ @@ -89,7 +95,12 @@ describe('correlations', () => { it('returns a query considering a custom field/value pair', () => { const query = getQueryWithParams({ - params: { index: 'apm-*', start: '2020', end: '2021' }, + params: { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, + }, fieldName: 'actualFieldName', fieldValue: 'actualFieldValue', }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts index aeb76c37e526..f28556f7a90b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts @@ -10,11 +10,11 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; -import { rangeRt } from '../../../routes/default_api_types'; -import { getCorrelationsFilters } from '../../correlations/get_filters'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { rangeRt } from '../../../../routes/default_api_types'; +import { getCorrelationsFilters } from '../../../correlations/get_filters'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; const getPercentileThresholdValueQuery = ( percentileThresholdValue: number | undefined diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts new file mode 100644 index 000000000000..e3f5c4a42d80 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.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 { getRequestBase } from './get_request_base'; + +describe('correlations', () => { + describe('getRequestBase', () => { + it('returns the request base parameters', () => { + const requestBase = getRequestBase({ + index: 'apm-*', + includeFrozen: true, + }); + expect(requestBase).toEqual({ + index: 'apm-*', + ignore_throttled: false, + ignore_unavailable: true, + }); + }); + + it('defaults ignore_throttled to true', () => { + const requestBase = getRequestBase({ + index: 'apm-*', + }); + expect(requestBase).toEqual({ + index: 'apm-*', + ignore_throttled: true, + ignore_unavailable: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts new file mode 100644 index 000000000000..e2cdbab830e0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts @@ -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 type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +export const getRequestBase = ({ + index, + includeFrozen, +}: SearchServiceFetchParams) => ({ + index, + // matches APM's event client settings + ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, + ignore_unavailable: true, +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts new file mode 100644 index 000000000000..c33b131d9cbd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; +export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; +export { fetchTransactionDurationFractions } from './query_fractions'; +export { fetchTransactionDurationPercentiles } from './query_percentiles'; +export { fetchTransactionDurationCorrelation } from './query_correlation'; +export { fetchTransactionDurationHistograms } from './query_histograms_generator'; +export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; +export { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts index 678328dce1a1..b1ab4aad3624 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts @@ -15,7 +15,12 @@ import { BucketCorrelation, } from './query_correlation'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const expectations = [1, 3, 5]; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; const fractions = [1, 2, 4, 5]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts index 94a708f67860..823abe936e22 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts @@ -9,10 +9,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export interface HistogramItem { key: number; @@ -88,7 +89,7 @@ export const getTransactionDurationCorrelationRequest = ( }, }; return { - index: params.index, + ...getRequestBase(params), body, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts index 8929b31b3ecb..fb707231245d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts @@ -9,14 +9,20 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { hasPrefixToInclude } from '../utils/has_prefix_to_include'; + import { fetchTransactionDurationFieldCandidates, getRandomDocsRequest, - hasPrefixToInclude, shouldBeExcluded, } from './query_field_candidates'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_field_candidates', () => { describe('shouldBeExcluded', () => { @@ -79,6 +85,8 @@ describe('query_field_candidates', () => { size: 1000, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts similarity index 85% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts index 8aa54e243eec..aeb67a4d6884 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts @@ -9,17 +9,19 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; -import { getQueryWithParams } from './get_query_with_params'; -import { Field } from './query_field_value_pairs'; import { - FIELD_PREFIX_TO_ADD_AS_CANDIDATE, FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, FIELDS_TO_ADD_AS_CANDIDATE, FIELDS_TO_EXCLUDE_AS_CANDIDATE, POPULATED_DOC_COUNT_SAMPLE_SIZE, -} from './constants'; +} from '../constants'; +import { hasPrefixToInclude } from '../utils'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; +import type { FieldName } from './query_field_value_pairs'; export const shouldBeExcluded = (fieldName: string) => { return ( @@ -30,16 +32,10 @@ export const shouldBeExcluded = (fieldName: string) => { ); }; -export const hasPrefixToInclude = (fieldName: string) => { - return FIELD_PREFIX_TO_ADD_AS_CANDIDATE.some((prefix) => - fieldName.startsWith(prefix) - ); -}; - export const getRandomDocsRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { fields: ['*'], _source: false, @@ -57,7 +53,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, params: SearchServiceFetchParams -): Promise<{ fieldCandidates: Field[] }> => { +): Promise<{ fieldCandidates: FieldName[] }> => { const { index } = params; // Get all fields with keyword mapping const respMapping = await esClient.fieldCaps({ diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts similarity index 81% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts index 7ffbc5208e41..0ee57fd60cd6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts @@ -9,14 +9,20 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { AsyncSearchProviderProgress } from '../../../../common/search_strategies/correlations/types'; +import { asyncSearchServiceLogProvider } from '../async_search_service_log'; +import { asyncSearchServiceStateProvider } from '../async_search_service_state'; import { fetchTransactionDurationFieldValuePairs, getTermsAggRequest, } from './query_field_value_pairs'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_field_value_pairs', () => { describe('getTermsAggRequest', () => { @@ -34,9 +40,6 @@ describe('query_field_value_pairs', () => { 'myFieldCandidate2', 'myFieldCandidate3', ]; - const progress = { - loadedFieldValuePairs: 0, - } as AsyncSearchProviderProgress; const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { body: estypes.SearchResponse; @@ -56,13 +59,19 @@ describe('query_field_value_pairs', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const state = asyncSearchServiceStateProvider(); + const resp = await fetchTransactionDurationFieldValuePairs( esClientMock, params, fieldCandidates, - progress + state, + addLogMessage ); + const { progress } = state.getState(); + expect(progress.loadedFieldValuePairs).toBe(1); expect(resp).toEqual([ { field: 'myFieldCandidate1', value: 'myValue1' }, @@ -73,6 +82,7 @@ describe('query_field_value_pairs', () => { { field: 'myFieldCandidate3', value: 'myValue2' }, ]); expect(esClientSearchMock).toHaveBeenCalledTimes(3); + expect(getLogMessages()).toEqual([]); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts new file mode 100644 index 000000000000..33adff4af7a5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { estypes } from '@elastic/elasticsearch'; + +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +import type { AsyncSearchServiceLog } from '../async_search_service_log'; +import type { AsyncSearchServiceState } from '../async_search_service_state'; +import { TERMS_SIZE } from '../constants'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; + +export type FieldName = string; + +interface FieldValuePair { + field: FieldName; + value: string; +} +export type FieldValuePairs = FieldValuePair[]; + +export const getTermsAggRequest = ( + params: SearchServiceFetchParams, + fieldName: FieldName +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params }), + size: 0, + aggs: { + attribute_terms: { + terms: { + field: fieldName, + size: TERMS_SIZE, + }, + }, + }, + }, +}); + +const fetchTransactionDurationFieldTerms = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldName: string, + addLogMessage: AsyncSearchServiceLog['addLogMessage'] +): Promise => { + try { + const resp = await esClient.search(getTermsAggRequest(params, fieldName)); + + if (resp.body.aggregations === undefined) { + addLogMessage( + `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs, no aggregations returned.`, + JSON.stringify(resp) + ); + return []; + } + const buckets = (resp.body.aggregations + .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ + key: string; + }>)?.buckets; + if (buckets?.length >= 1) { + return buckets.map((d) => ({ + field: fieldName, + value: d.key, + })); + } + } catch (e) { + addLogMessage( + `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs.`, + JSON.stringify(e) + ); + } + + return []; +}; + +async function fetchInSequence( + fieldCandidates: FieldName[], + fn: (fieldCandidate: string) => Promise +) { + const results = []; + + for (const fieldCandidate of fieldCandidates) { + results.push(...(await fn(fieldCandidate))); + } + + return results; +} + +export const fetchTransactionDurationFieldValuePairs = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldCandidates: FieldName[], + state: AsyncSearchServiceState, + addLogMessage: AsyncSearchServiceLog['addLogMessage'] +): Promise => { + let fieldValuePairsProgress = 1; + + return await fetchInSequence( + fieldCandidates, + async function (fieldCandidate: string) { + const fieldTerms = await fetchTransactionDurationFieldTerms( + esClient, + params, + fieldCandidate, + addLogMessage + ); + + state.setProgress({ + loadedFieldValuePairs: fieldValuePairsProgress / fieldCandidates.length, + }); + fieldValuePairsProgress++; + + return fieldTerms; + } + ); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts index 3e7d4a52e4de..a44cc6131b4c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationRangesRequest, } from './query_fractions'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; describe('query_fractions', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts similarity index 86% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts index e9cec25673c6..35e59054ad01 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts @@ -7,15 +7,18 @@ import { ElasticsearchClient } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; -import { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; + +import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; + import { getQueryWithParams } from './get_query_with_params'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( params: SearchServiceFetchParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts index ace917794796..daa40725b730 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationHistogramRequest, } from './query_histogram'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const interval = 100; describe('query_histogram', () => { @@ -54,7 +59,9 @@ describe('query_histogram', () => { }, size: 0, }, - index: 'apm-*', + index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts similarity index 74% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts index 045caabeab26..18fc18af1472 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts @@ -9,36 +9,33 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; import type { HistogramItem, ResponseHit, SearchServiceFetchParams, -} from '../../../../common/search_strategies/correlations/types'; +} from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export const getTransactionDurationHistogramRequest = ( params: SearchServiceFetchParams, interval: number, fieldName?: string, fieldValue?: string -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, fieldName, fieldValue }); - - return { - index: params.index, - body: { - query, - size: 0, - aggs: { - transaction_duration_histogram: { - histogram: { field: TRANSACTION_DURATION, interval }, - }, +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params, fieldName, fieldValue }), + size: 0, + aggs: { + transaction_duration_histogram: { + histogram: { field: TRANSACTION_DURATION, interval }, }, }, - }; -}; + }, +}); export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts index ebd78f124851..b40f685645a2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts @@ -14,7 +14,12 @@ import { getHistogramIntervalRequest, } from './query_histogram_interval'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_histogram_interval', () => { describe('getHistogramIntervalRequest', () => { @@ -58,6 +63,8 @@ describe('query_histogram_interval', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts similarity index 84% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts index 0f897f2e9236..cc50c8d4d860 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts @@ -9,17 +9,18 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; const HISTOGRAM_INTERVALS = 1000; export const getHistogramIntervalRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts index 76aab1cd979c..b909778e3e4e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts @@ -14,7 +14,12 @@ import { getHistogramIntervalRequest, } from './query_histogram_range_steps'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_histogram_range_steps', () => { describe('getHistogramIntervalRequest', () => { @@ -58,6 +63,8 @@ describe('query_histogram_range_steps', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts index 405c84828db4..116b5d164560 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts @@ -11,10 +11,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; const getHistogramRangeSteps = (min: number, max: number, steps: number) => { // A d3 based scale function as a helper to get equally distributed bins on a log scale. @@ -27,7 +28,7 @@ const getHistogramRangeSteps = (min: number, max: number, steps: number) => { export const getHistogramIntervalRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts new file mode 100644 index 000000000000..7f89aa52367a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { asyncSearchServiceLogProvider } from '../async_search_service_log'; +import { asyncSearchServiceStateProvider } from '../async_search_service_state'; + +import { fetchTransactionDurationHistograms } from './query_histograms_generator'; + +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; +const expectations = [1, 3, 5]; +const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; +const fractions = [1, 2, 4, 5]; +const totalDocCount = 1234; +const histogramRangeSteps = [1, 2, 4, 5]; + +const fieldValuePairs = [ + { field: 'the-field-name-1', value: 'the-field-value-1' }, + { field: 'the-field-name-2', value: 'the-field-value-2' }, + { field: 'the-field-name-2', value: 'the-field-value-3' }, +]; + +describe('query_histograms_generator', () => { + describe('fetchTransactionDurationHistograms', () => { + it(`doesn't break on failing ES queries and adds messages to the log`, async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({} as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const state = asyncSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + let loadedHistograms = 0; + const items = []; + + for await (const item of fetchTransactionDurationHistograms( + esClientMock, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { + if (item !== undefined) { + items.push(item); + } + loadedHistograms++; + } + + expect(items.length).toEqual(0); + expect(loadedHistograms).toEqual(3); + expect(esClientSearchMock).toHaveBeenCalledTimes(3); + expect(getLogMessages().map((d) => d.split(': ')[1])).toEqual([ + "Failed to fetch correlation/kstest for 'the-field-name-1/the-field-value-1'", + "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-2'", + "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-3'", + ]); + }); + + it('returns items with correlation and ks-test value', async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + latency_ranges: { buckets: [] }, + transaction_duration_correlation: { value: 0.6 }, + ks_test: { less: 0.001 }, + logspace_ranges: { buckets: [] }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const state = asyncSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + let loadedHistograms = 0; + const items = []; + + for await (const item of fetchTransactionDurationHistograms( + esClientMock, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { + if (item !== undefined) { + items.push(item); + } + loadedHistograms++; + } + + expect(items.length).toEqual(3); + expect(loadedHistograms).toEqual(3); + expect(esClientSearchMock).toHaveBeenCalledTimes(6); + expect(getLogMessages().length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts new file mode 100644 index 000000000000..c4869aac187c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +import type { AsyncSearchServiceLog } from '../async_search_service_log'; +import type { AsyncSearchServiceState } from '../async_search_service_state'; +import { CORRELATION_THRESHOLD, KS_TEST_THRESHOLD } from '../constants'; + +import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; +import { fetchTransactionDurationCorrelation } from './query_correlation'; +import { fetchTransactionDurationRanges } from './query_ranges'; + +import type { FieldValuePairs } from './query_field_value_pairs'; + +export async function* fetchTransactionDurationHistograms( + esClient: ElasticsearchClient, + addLogMessage: AsyncSearchServiceLog['addLogMessage'], + params: SearchServiceFetchParams, + state: AsyncSearchServiceState, + expectations: number[], + ranges: estypes.AggregationsAggregationRange[], + fractions: number[], + histogramRangeSteps: number[], + totalDocCount: number, + fieldValuePairs: FieldValuePairs +) { + for (const item of getPrioritizedFieldValuePairs(fieldValuePairs)) { + if (params === undefined || item === undefined || state.getIsCancelled()) { + state.setIsRunning(false); + return; + } + + // If one of the fields have an error + // We don't want to stop the whole process + try { + const { correlation, ksTest } = await fetchTransactionDurationCorrelation( + esClient, + params, + expectations, + ranges, + fractions, + totalDocCount, + item.field, + item.value + ); + + if (state.getIsCancelled()) { + state.setIsRunning(false); + return; + } + + if ( + correlation !== null && + correlation > CORRELATION_THRESHOLD && + ksTest !== null && + ksTest < KS_TEST_THRESHOLD + ) { + const logHistogram = await fetchTransactionDurationRanges( + esClient, + params, + histogramRangeSteps, + item.field, + item.value + ); + yield { + ...item, + correlation, + ksTest, + histogram: logHistogram, + }; + } else { + yield undefined; + } + } catch (e) { + // don't fail the whole process for individual correlation queries, + // just add the error to the internal log and check if we'd want to set the + // cross-cluster search compatibility warning to true. + addLogMessage( + `Failed to fetch correlation/kstest for '${item.field}/${item.value}'`, + JSON.stringify(e) + ); + if (params?.index.includes(':')) { + state.setCcsWarning(true); + } + yield undefined; + } + } +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts index f0d01a4849f9..2ca77f51be07 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationPercentilesRequest, } from './query_percentiles'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_percentiles', () => { describe('getTransactionDurationPercentilesRequest', () => { @@ -57,6 +62,8 @@ describe('query_percentiles', () => { track_total_hits: true, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts index cb302f19a000..bd230687314e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts @@ -9,11 +9,12 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; -import { SIGNIFICANT_VALUE_DIGITS } from './constants'; +import { getRequestBase } from './get_request_base'; +import { SIGNIFICANT_VALUE_DIGITS } from '../constants'; export interface HistogramItem { key: number; @@ -36,7 +37,7 @@ export const getTransactionDurationPercentilesRequest = ( const query = getQueryWithParams({ params, fieldName, fieldValue }); return { - index: params.index, + ...getRequestBase(params), body: { track_total_hits: true, query, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts index 7d18efc36056..fc995ae07b3d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationRangesRequest, } from './query_ranges'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const rangeSteps = [1, 3, 5]; describe('query_ranges', () => { @@ -74,6 +79,8 @@ describe('query_ranges', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts similarity index 88% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts index 0e813a18fdf4..6f662363d0c4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts @@ -9,10 +9,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export interface HistogramItem { key: number; @@ -47,7 +48,7 @@ export const getTransactionDurationRangesRequest = ( } return { - index: params.index, + ...getRequestBase(params), body: { query, size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts deleted file mode 100644 index 23928565da08..000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts +++ /dev/null @@ -1,89 +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 type { ElasticsearchClient } from 'src/core/server'; - -import type { estypes } from '@elastic/elasticsearch'; - -import type { - AsyncSearchProviderProgress, - SearchServiceFetchParams, -} from '../../../../common/search_strategies/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { TERMS_SIZE } from './constants'; - -interface FieldValuePair { - field: string; - value: string; -} -type FieldValuePairs = FieldValuePair[]; - -export type Field = string; - -export const getTermsAggRequest = ( - params: SearchServiceFetchParams, - fieldName: string -): estypes.SearchRequest => ({ - index: params.index, - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - attribute_terms: { - terms: { - field: fieldName, - size: TERMS_SIZE, - }, - }, - }, - }, -}); - -export const fetchTransactionDurationFieldValuePairs = async ( - esClient: ElasticsearchClient, - params: SearchServiceFetchParams, - fieldCandidates: Field[], - progress: AsyncSearchProviderProgress -): Promise => { - const fieldValuePairs: FieldValuePairs = []; - - let fieldValuePairsProgress = 1; - - for (let i = 0; i < fieldCandidates.length; i++) { - const fieldName = fieldCandidates[i]; - // mutate progress - progress.loadedFieldValuePairs = - fieldValuePairsProgress / fieldCandidates.length; - - try { - const resp = await esClient.search(getTermsAggRequest(params, fieldName)); - - if (resp.body.aggregations === undefined) { - fieldValuePairsProgress++; - continue; - } - const buckets = (resp.body.aggregations - .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ - key: string; - }>)?.buckets; - if (buckets.length >= 1) { - fieldValuePairs.push( - ...buckets.map((d) => ({ - field: fieldName, - value: d.key, - })) - ); - } - - fieldValuePairsProgress++; - } catch (e) { - fieldValuePairsProgress++; - } - } - return fieldValuePairs; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts index bdb2ffbfcd89..8fb4ecc57e7a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { computeExpectationsAndRanges } from './aggregation_utils'; +import { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; describe('aggregation utils', () => { describe('computeExpectationsAndRanges', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts new file mode 100644 index 000000000000..2034c29b01d9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { currentTimeAsString } from './current_time_as_string'; + +describe('aggregation utils', () => { + describe('currentTimeAsString', () => { + it('returns the current time as a string', () => { + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + + const timeString = currentTimeAsString(); + + expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); + + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts new file mode 100644 index 000000000000..f454b8c8274f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.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 currentTimeAsString = () => new Date().toISOString(); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts new file mode 100644 index 000000000000..a951dc63caad --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FIELD_PREFIX_TO_ADD_AS_CANDIDATE } from '../constants'; + +import { hasPrefixToInclude } from './has_prefix_to_include'; + +describe('aggregation utils', () => { + describe('hasPrefixToInclude', () => { + it('returns true if the prefix is included', async () => { + FIELD_PREFIX_TO_ADD_AS_CANDIDATE.forEach((prefix) => { + expect(hasPrefixToInclude(`${prefix}the-field-name`)).toBe(true); + }); + }); + it('returns false if the prefix is included', async () => { + FIELD_PREFIX_TO_ADD_AS_CANDIDATE.forEach((prefix) => { + expect( + hasPrefixToInclude(`unknown-prefix-.${prefix}the-field-name`) + ).toBe(false); + expect(hasPrefixToInclude('the-field-name')).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts new file mode 100644 index 000000000000..baf4d62af00f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FIELD_PREFIX_TO_ADD_AS_CANDIDATE } from '../constants'; + +export const hasPrefixToInclude = (fieldName: string) => { + return FIELD_PREFIX_TO_ADD_AS_CANDIDATE.some((prefix) => + fieldName.startsWith(prefix) + ); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts index ab6190fb288a..000fd57c718b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export * from './math_utils'; -export * from './aggregation_utils'; +export { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; +export { currentTimeAsString } from './current_time_as_string'; +export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts deleted file mode 100644 index ed4107b9d602..000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts +++ /dev/null @@ -1,26 +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 { getRandomInt } from './math_utils'; - -describe('math utils', () => { - describe('getRandomInt', () => { - it('returns a random integer within the given range', () => { - const min = 0.9; - const max = 11.1; - const randomInt = getRandomInt(min, max); - expect(Number.isInteger(randomInt)).toBe(true); - expect(randomInt > min).toBe(true); - expect(randomInt < max).toBe(true); - }); - - it('returns 1 if given range only allows this integer', () => { - const randomInt = getRandomInt(0.9, 1.1); - expect(randomInt).toBe(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts deleted file mode 100644 index 01e856e511fc..000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts +++ /dev/null @@ -1,70 +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 { range } from 'lodash'; -import { HistogramItem } from '../query_ranges'; -import { asPreciseDecimal } from '../../../../../common/utils/formatters'; - -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random -export function getRandomInt(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive -} - -// Roughly compare histograms by sampling random bins -// And rounding up histogram count to account for different floating points -export const isHistogramRoughlyEqual = ( - a: HistogramItem[], - b: HistogramItem[], - { numBinsToSample = 10, significantFraction = 3 } -) => { - if (a.length !== b.length) return false; - - const sampledIndices = Array.from(Array(numBinsToSample).keys()).map(() => - getRandomInt(0, a.length - 1) - ); - return !sampledIndices.some((idx) => { - return ( - asPreciseDecimal(a[idx].key, significantFraction) !== - asPreciseDecimal(b[idx].key, significantFraction) && - roundToNearest(a[idx].doc_count) !== roundToNearest(b[idx].doc_count) - ); - }); -}; - -/** Round numeric to the nearest 5 - * E.g. if roundBy = 5, results will be 11 -> 10, 14 -> 10, 16 -> 20 - */ -export const roundToNearest = (n: number, roundBy = 5) => { - return Math.ceil((n + 1) / roundBy) * roundBy; -}; - -/** - * Create a rough stringified version of the histogram - */ -export const hashHistogram = ( - histogram: HistogramItem[], - { significantFraction = 3, numBinsToSample = 10 } -) => { - // Generate bins to sample evenly - const sampledIndices = Array.from( - range( - 0, - histogram.length - 1, - Math.ceil(histogram.length / numBinsToSample) - ) - ); - return JSON.stringify( - sampledIndices.map((idx) => { - return `${asPreciseDecimal( - histogram[idx].key, - significantFraction - )}-${roundToNearest(histogram[idx].doc_count)}`; - }) - ); -}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index a9fe55456ad4..4cbc62d87eff 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { EVENT_KIND } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { EVENT_KIND as EVENT_KIND_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { EVENT_KIND as EVENT_KIND_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { RuleDataClient } from '../../../../rule_registry/server'; import { SERVICE_NAME, @@ -14,6 +16,8 @@ import { import { rangeQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; +const EVENT_KIND: typeof EVENT_KIND_TYPED = EVENT_KIND_NON_TYPED; + export async function getServiceAlerts({ ruleDataClient, start, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts index 1e0c8e6f6044..d277967dad77 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts @@ -159,7 +159,7 @@ export async function getServiceErrorGroupPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'groupId'), @@ -167,7 +167,7 @@ export async function getServiceErrorGroupPeriods({ previousPeriod.map((errorRateGroup) => ({ ...errorRateGroup, timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.timeseries, + currentPeriodTimeseries: firstCurrentPeriod?.timeseries, previousPeriodTimeseries: errorRateGroup.timeseries, }), })), diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts new file mode 100644 index 000000000000..79ecab45c75b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; +import { rangeQuery, kqlQuery } from '../../../../observability/server'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { + SERVICE_NAME, + CONTAINER_ID, + HOSTNAME, + POD_NAME, +} from '../../../common/elasticsearch_fieldnames'; + +export const getServiceInfrastructure = async ({ + kuery, + serviceName, + environment, + setup, +}: { + kuery?: string; + serviceName: string; + environment?: string; + setup: Setup & SetupTimeRange; +}) => { + const { apmEventClient, start, end } = setup; + + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; + + const response = await apmEventClient.search('get_service_infrastructure', { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter, + }, + }, + aggs: { + containerIds: { + terms: { + field: CONTAINER_ID, + size: 500, + }, + }, + hostNames: { + terms: { + field: HOSTNAME, + size: 500, + }, + }, + podNames: { + terms: { + field: POD_NAME, + size: 500, + }, + }, + }, + }, + }); + + return { + containerIds: + response.aggregations?.containerIds?.buckets.map( + (bucket) => bucket.key + ) ?? [], + hostNames: + response.aggregations?.hostNames?.buckets.map((bucket) => bucket.key) ?? + [], + podNames: + response.aggregations?.podNames?.buckets.map((bucket) => bucket.key) ?? + [], + }; +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts index 804ed91c54a5..fc91efbc7c6e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts @@ -123,9 +123,7 @@ export async function getServiceInstancesDetailedStatisticsPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length - ? currentPeriod[0] - : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'serviceNodeName'), @@ -134,23 +132,23 @@ export async function getServiceInstancesDetailedStatisticsPeriods({ return { ...data, cpuUsage: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.cpuUsage, + currentPeriodTimeseries: firstCurrentPeriod?.cpuUsage, previousPeriodTimeseries: data.cpuUsage, }), errorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.errorRate, + currentPeriodTimeseries: firstCurrentPeriod?.errorRate, previousPeriodTimeseries: data.errorRate, }), latency: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.latency, + currentPeriodTimeseries: firstCurrentPeriod?.latency, previousPeriodTimeseries: data.latency, }), memoryUsage: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.memoryUsage, + currentPeriodTimeseries: firstCurrentPeriod?.memoryUsage, previousPeriodTimeseries: data.memoryUsage, }), throughput: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.throughput, + currentPeriodTimeseries: firstCurrentPeriod?.throughput, previousPeriodTimeseries: data.throughput, }), }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts index 6edec60b6f37..9d5b173a1c7e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts @@ -29,7 +29,7 @@ import { getLatencyValue, } from '../helpers/latency_aggregation_type'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; +import { calculateFailedTransactionRate } from '../helpers/transaction_error_rate'; export async function getServiceTransactionGroupDetailedStatistics({ environment, @@ -164,7 +164,7 @@ export async function getServiceTransactionGroupDetailedStatistics({ })); const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ x: timeseriesBucket.key, - y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]), + y: calculateFailedTransactionRate(timeseriesBucket[EVENT_OUTCOME]), })); const transactionGroupTotalDuration = bucket.transaction_group_total_duration.value || 0; @@ -239,7 +239,7 @@ export async function getServiceTransactionGroupDetailedStatisticsPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'transactionName'), @@ -248,15 +248,15 @@ export async function getServiceTransactionGroupDetailedStatisticsPeriods({ return { ...data, errorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.errorRate, + currentPeriodTimeseries: firstCurrentPeriod?.errorRate, previousPeriodTimeseries: data.errorRate, }), throughput: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.throughput, + currentPeriodTimeseries: firstCurrentPeriod?.throughput, previousPeriodTimeseries: data.throughput, }), latency: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.latency, + currentPeriodTimeseries: firstCurrentPeriod?.latency, previousPeriodTimeseries: data.latency, }), }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index e3f2795eb38e..c8ae36946a2f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -26,7 +26,7 @@ import { getLatencyValue, } from '../helpers/latency_aggregation_type'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; +import { calculateFailedTransactionRate } from '../helpers/transaction_error_rate'; export type ServiceOverviewTransactionGroupSortField = | 'name' @@ -115,9 +115,7 @@ export async function getServiceTransactionGroups({ const transactionGroups = response.aggregations?.transaction_groups.buckets.map((bucket) => { - const errorRate = calculateTransactionErrorPercentage( - bucket[EVENT_OUTCOME] - ); + const errorRate = calculateFailedTransactionRate(bucket[EVENT_OUTCOME]); const transactionGroupTotalDuration = bucket.transaction_group_total_duration.value || 0; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index edc9e5cf9002..885d73b13bb9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -25,7 +25,7 @@ import { } from '../../helpers/aggregated_transactions'; import { calculateThroughput } from '../../helpers/calculate_throughput'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; import { ServicesItemsSetup } from './get_services_items'; @@ -137,7 +137,7 @@ export async function getServiceTransactionStats({ AGENT_NAME ] as AgentName, latency: topTransactionTypeBucket.avg_duration.value, - transactionErrorRate: calculateTransactionErrorPercentage( + transactionErrorRate: calculateFailedTransactionRate( topTransactionTypeBucket.outcomes ), throughput: calculateThroughput({ diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index d339641069eb..90fa54f31eb3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -26,7 +26,7 @@ import { calculateThroughput } from '../../helpers/calculate_throughput'; import { getBucketSizeForAggregatedTransactions } from '../../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; @@ -148,7 +148,7 @@ export async function getServiceTransactionDetailedStatistics({ transactionErrorRate: topTransactionTypeBucket.timeseries.buckets.map( (dateBucket) => ({ x: dateBucket.key + offsetInMs, - y: calculateTransactionErrorPercentage(dateBucket.outcomes), + y: calculateFailedTransactionRate(dateBucket.outcomes), }) ), throughput: topTransactionTypeBucket.timeseries.buckets.map( diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 4004d55da79f..b7f265daeb46 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -16,7 +16,6 @@ import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../helpers/aggregated_transactions'; -import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup } from '../helpers/setup_request'; interface Options { @@ -28,9 +27,11 @@ interface Options { transactionType: string; start: number; end: number; + intervalString: string; + throughputUnit: 'minute' | 'second'; } -function fetcher({ +export async function getThroughput({ environment, kuery, searchAggregatedTransactions, @@ -39,13 +40,11 @@ function fetcher({ transactionType, start, end, + intervalString, + throughputUnit, }: Options) { const { apmEventClient } = setup; - const { intervalString } = getBucketSizeForAggregatedTransactions({ - start, - end, - searchAggregatedTransactions, - }); + const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, @@ -79,7 +78,7 @@ function fetcher({ aggs: { throughput: { rate: { - unit: 'minute' as const, + unit: throughputUnit, }, }, }, @@ -88,11 +87,10 @@ function fetcher({ }, }; - return apmEventClient.search('get_throughput_for_service', params); -} - -export async function getThroughput(options: Options) { - const response = await fetcher(options); + const response = await apmEventClient.search( + 'get_throughput_for_service', + params + ); return ( response.aggregations?.timeseries.buckets.map((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index b27a54a98373..5c3395939910 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -23,9 +23,9 @@ import { import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../helpers/transaction_error_rate'; export async function getErrorRate({ @@ -124,13 +124,11 @@ export async function getErrorRate({ return { noHits, transactionErrorRate: [], average: null }; } - const transactionErrorRate = getTransactionErrorRateTimeSeries( + const transactionErrorRate = getFailedTransactionRateTimeSeries( resp.aggregations.timeseries.buckets ); - const average = calculateTransactionErrorPercentage( - resp.aggregations.outcomes - ); + const average = calculateFailedTransactionRate(resp.aggregations.outcomes); return { noHits, transactionErrorRate, average }; } @@ -183,16 +181,14 @@ export async function getErrorRatePeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.transactionErrorRate.length - ? currentPeriod.transactionErrorRate - : undefined; + const firstCurrentPeriod = currentPeriod.transactionErrorRate; return { currentPeriod, previousPeriod: { ...previousPeriod, transactionErrorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod, + currentPeriodTimeseries: firstCurrentPeriod, previousPeriodTimeseries: previousPeriod.transactionErrorRate, }), }, diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts index e673770dbbc1..c5c2b3cac228 100644 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ b/x-pack/plugins/apm/server/routes/backends.ts @@ -237,7 +237,7 @@ const backendThroughputChartsRoute = createApmServerRoute({ }, }); -const backendErrorRateChartsRoute = createApmServerRoute({ +const backendFailedTransactionRateChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate', params: t.type({ path: t.type({ @@ -288,4 +288,4 @@ export const backendsRouteRepository = createApmServerRouteRepository() .add(backendMetadataRoute) .add(backendLatencyChartsRoute) .add(backendThroughputChartsRoute) - .add(backendErrorRateChartsRoute); + .add(backendFailedTransactionRateChartsRoute); diff --git a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts new file mode 100644 index 000000000000..3011d0c3864e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { getFallbackToTransactions } from '../lib/helpers/aggregated_transactions/get_fallback_to_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { kueryRt, rangeRt } from './default_api_types'; + +const fallbackToTransactionsRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/fallback_to_transactions', + params: t.partial({ + query: t.intersection([kueryRt, t.partial(rangeRt.props)]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { + params: { + query: { kuery }, + }, + } = resources; + return { + fallbackToTransactions: await getFallbackToTransactions({ setup, kuery }), + }; + }, +}); + +export const fallbackToTransactionsRouteRepository = createApmServerRouteRepository().add( + fallbackToTransactionsRoute +); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index ad4b48c090e5..b66daf80bd76 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -21,6 +21,7 @@ import { indexPatternRouteRepository } from './index_pattern'; import { metricsRouteRepository } from './metrics'; import { observabilityOverviewRouteRepository } from './observability_overview'; import { rumRouteRepository } from './rum_client'; +import { fallbackToTransactionsRouteRepository } from './fallback_to_transactions'; import { serviceRouteRepository } from './services'; import { serviceMapRouteRepository } from './service_map'; import { serviceNodeRouteRepository } from './service_nodes'; @@ -54,7 +55,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(customLinkRouteRepository) .merge(sourceMapsRouteRepository) .merge(apmFleetRouteRepository) - .merge(backendsRouteRepository); + .merge(backendsRouteRepository) + .merge(fallbackToTransactionsRouteRepository); return repository; }; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index f5156fe85fbf..10d5bc5e3abd 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -12,6 +12,7 @@ import { uniq } from 'lodash'; import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; import { ProfilingValueType } from '../../common/profiling'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getThroughputUnit } from '../lib/helpers/calculate_throughput'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; @@ -30,6 +31,7 @@ import { getServiceTransactionTypes } from '../lib/services/get_service_transact import { getThroughput } from '../lib/services/get_throughput'; import { getServiceProfilingStatistics } from '../lib/services/profiling/get_service_profiling_statistics'; import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; +import { getServiceInfrastructure } from '../lib/services/get_service_infrastructure'; import { withApmSpan } from '../utils/with_apm_span'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; @@ -43,6 +45,7 @@ import { import { offsetPreviousPeriodCoordinates } from '../../common/utils/offset_previous_period_coordinate'; import { getServicesDetailedStatistics } from '../lib/services/get_services_detailed_statistics'; import { getServiceDependenciesBreakdown } from '../lib/services/get_service_dependencies_breakdown'; +import { getBucketSizeForAggregatedTransactions } from '../lib/helpers/get_bucket_size_for_aggregated_transactions'; const servicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services', @@ -451,6 +454,16 @@ const serviceThroughputRoute = createApmServerRoute({ }); const { start, end } = setup; + const { + bucketSize, + intervalString, + } = getBucketSizeForAggregatedTransactions({ + start, + end, + searchAggregatedTransactions, + }); + + const throughputUnit = getThroughputUnit(bucketSize); const commonProps = { environment, @@ -459,6 +472,8 @@ const serviceThroughputRoute = createApmServerRoute({ serviceName, setup, transactionType, + throughputUnit, + intervalString, }; const [currentPeriod, previousPeriod] = await Promise.all([ @@ -482,6 +497,7 @@ const serviceThroughputRoute = createApmServerRoute({ currentPeriodTimeseries: currentPeriod, previousPeriodTimeseries: previousPeriod, }), + throughputUnit, }; }, }); @@ -838,6 +854,35 @@ const serviceAlertsRoute = createApmServerRoute({ }, }); +const serviceInfrastructureRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/infrastructure', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([kueryRt, rangeRt, environmentRt]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; + + const { + path: { serviceName }, + query: { environment, kuery }, + } = params; + + const serviceInfrastructure = await getServiceInfrastructure({ + setup, + serviceName, + environment, + kuery, + }); + return { serviceInfrastructure }; + }, +}); + export const serviceRouteRepository = createApmServerRouteRepository() .add(servicesRoute) .add(servicesDetailedStatisticsRoute) @@ -858,4 +903,5 @@ export const serviceRouteRepository = createApmServerRouteRepository() .add(serviceDependenciesBreakdownRoute) .add(serviceProfilingTimelineRoute) .add(serviceProfilingStatisticsRoute) - .add(serviceAlertsRoute); + .add(serviceAlertsRoute) + .add(serviceInfrastructureRoute); diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 192b7f4fe8c2..6eaf1a3bf183 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json index 85608a8a78ad..48767fb4525f 100644 --- a/x-pack/plugins/banners/tsconfig.json +++ b/x-pack/plugins/banners/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index 6feb22b2ef15..7401bbb6aabe 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { SerializableRecord } from '@kbn/utility-types'; import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; -import { Datatable, ExecutionContext, SerializableState } from 'src/plugins/expressions'; +import { Datatable, ExecutionContext } from 'src/plugins/expressions'; import { Adapters } from 'src/plugins/inspector'; const errors = getFunctionErrors().csv; @@ -39,7 +40,7 @@ one,1 two,2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -55,7 +56,7 @@ two\t2 fourty two\t42`, delimiter: '\t', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); @@ -69,7 +70,7 @@ two%SPLIT%2 fourty two%SPLIT%42`, delimiter: '%SPLIT%', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -82,7 +83,7 @@ fourty two%SPLIT%42`, data: `name,number\rone,1\rtwo,2\rfourty two,42`, newline: '\r', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -106,7 +107,7 @@ fourty two%SPLIT%42`, data: `foo," bar ", baz, " buz " 1,2,3,4`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -134,7 +135,7 @@ fourty two%SPLIT%42`, 1," best ",3, " ok" " good", bad, better , " worst " `, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -149,7 +150,7 @@ one|1 two.2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ); }).toThrow(new RegExp(errors.invalidInputCSV().message)); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts index 6f785f1b9d47..b64761bb5a82 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts @@ -8,8 +8,9 @@ import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib'; import { testTable, relationalTable } from './__fixtures__/test_tables'; import { dropdownControl } from './dropdownControl'; -import { ExecutionContext, SerializableState } from 'src/plugins/expressions'; +import { ExecutionContext } from 'src/plugins/expressions'; import { Adapters } from 'src/plugins/inspector'; +import { SerializableRecord } from '@kbn/utility-types'; describe('dropdownControl', () => { const fn = functionWrapper(dropdownControl); @@ -19,14 +20,14 @@ describe('dropdownControl', () => { fn( testTable, { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext ) ).toHaveProperty('type', 'render'); expect( fn( testTable, { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext ) ).toHaveProperty('as', 'dropdown_filter'); }); @@ -43,18 +44,21 @@ describe('dropdownControl', () => { fn( testTable, { valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext )?.value?.choices ).toEqual(uniqueNames); }); it('returns an empty array when provided an invalid column', () => { expect( - fn(testTable, { valueColumn: 'foo' }, {} as ExecutionContext) - ?.value?.choices + fn( + testTable, + { valueColumn: 'foo' }, + {} as ExecutionContext + )?.value?.choices ).toEqual([]); expect( - fn(testTable, { valueColumn: '' }, {} as ExecutionContext) + fn(testTable, { valueColumn: '' }, {} as ExecutionContext) ?.value?.choices ).toEqual([]); }); @@ -67,7 +71,7 @@ describe('dropdownControl', () => { fn( relationalTable, { valueColumn: 'id', labelColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext )?.value?.choices ).toEqual(expectedChoices); }); @@ -77,22 +81,28 @@ describe('dropdownControl', () => { describe('filterColumn', () => { it('sets which column the filter is applied to', () => { expect( - fn(testTable, { filterColumn: 'name' }, {} as ExecutionContext) - ?.value + fn( + testTable, + { filterColumn: 'name' }, + {} as ExecutionContext + )?.value ).toHaveProperty('column', 'name'); expect( fn( testTable, { filterColumn: 'name', valueColumn: 'price' }, - {} as ExecutionContext + {} as ExecutionContext )?.value ).toHaveProperty('column', 'name'); }); it('defaults to valueColumn if not provided', () => { expect( - fn(testTable, { valueColumn: 'price' }, {} as ExecutionContext) - ?.value + fn( + testTable, + { valueColumn: 'price' }, + {} as ExecutionContext + )?.value ).toHaveProperty('column', 'price'); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index ffa1557d2b54..d0ea8fff0a45 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -56,25 +56,6 @@ describe('switch', () => { }); describe('function', () => { - describe('with no cases', () => { - it('should return the context if no default is provided', () => { - const context = 'foo'; - - testScheduler.run(({ expectObservable }) => - expectObservable(fn(context, {})).toBe('(0|)', [context]) - ); - }); - - it('should return the default if provided', () => { - const context = 'foo'; - const args = { default: () => of('bar') }; - - testScheduler.run(({ expectObservable }) => - expectObservable(fn(context, args)).toBe('(0|)', ['bar']) - ); - }); - }); - describe('with no matching cases', () => { it('should return the context if no default is provided', () => { const context = 'foo'; @@ -108,6 +89,55 @@ describe('switch', () => { expectObservable(fn(context, args)).toBe('(0|)', [result]) ); }); + + it('should support partial results', () => { + testScheduler.run(({ cold, expectObservable }) => { + const context = 'foo'; + const case1 = cold('--ab-c-', { + a: { + type: 'case', + matches: false, + result: 1, + }, + b: { + type: 'case', + matches: true, + result: 2, + }, + c: { + type: 'case', + matches: false, + result: 3, + }, + }); + const case2 = cold('-a--bc-', { + a: { + type: 'case', + matches: true, + result: 4, + }, + b: { + type: 'case', + matches: true, + result: 5, + }, + c: { + type: 'case', + matches: true, + result: 6, + }, + }); + const expected = ' --abc(de)-'; + const args = { case: [() => case1, () => case2] }; + expectObservable(fn(context, args)).toBe(expected, { + a: 4, + b: 2, + c: 2, + d: 5, + e: 6, + }); + }); + }); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index f4e6c92c91cb..3e676c829a30 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { Observable, defer, from, of } from 'rxjs'; -import { concatMap, filter, merge, pluck, take } from 'rxjs/operators'; +import { Observable, combineLatest, defer, of } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Case } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - case?: Array<() => Observable>; + case: Array<() => Observable>; default?(): Observable; } @@ -43,12 +43,13 @@ export function switchFn(): ExpressionFunctionDefinition< }, }, fn(input, args) { - return from(args.case ?? []).pipe( - concatMap((item) => item()), - filter(({ matches }) => matches), - pluck('result'), - merge(defer(() => args.default?.() ?? of(input))), - take(1) + return combineLatest(args.case.map((item) => defer(() => item()))).pipe( + concatMap((items) => { + const item = items.find(({ matches }) => matches); + const item$ = item && of(item.result); + + return item$ ?? args.default?.() ?? of(input); + }) ); }, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index b6343f5ca7ca..bc77f347530b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -5,142 +5,128 @@ * 2.0. */ -import React, { Component } from 'react'; -import { compose, withPropsOnChange, withHandlers } from 'recompose'; +import React, { useState, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiSelect, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { sortBy } from 'lodash'; import { getType } from '@kbn/interpreter/common'; -import { createStatefulPropHoc } from '../../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../../i18n'; import { SimpleMathFunction } from './simple_math_function'; import { getFormObject } from './get_form_object'; const { DataColumn: strings } = ArgumentStrings; + const maybeQuoteValue = (val) => (val.match(/\s/) ? `'${val}'` : val); +const valueNotSet = (val) => !val || val.length === 0; -// TODO: Garbage, we could make a much nicer math form that can handle way more. -class DatacolumnArgInput extends Component { - static propTypes = { - columns: PropTypes.array.isRequired, - onValueChange: PropTypes.func.isRequired, - mathValue: PropTypes.object.isRequired, - setMathFunction: PropTypes.func.isRequired, - typeInstance: PropTypes.object.isRequired, - renderError: PropTypes.func.isRequired, - argId: PropTypes.string.isRequired, - }; - - inputRefs = {}; - - render() { - const { - onValueChange, - columns, - mathValue, - setMathFunction, - renderError, - argId, - typeInstance, - } = this.props; - - if (mathValue.error) { - renderError(); - return null; - } - - const allowedTypes = typeInstance.options.allowedTypes || false; - const onlyShowMathFunctions = typeInstance.options.onlyMath || false; - const valueNotSet = (val) => !val || val.length === 0; - - const updateFunctionValue = () => { - const fn = this.inputRefs.fn.value; - const column = this.inputRefs.column.value; +const getMathValue = (argValue, columns) => { + if (getType(argValue) !== 'string') { + return { error: 'argValue is not a string type' }; + } + try { + const matchedCol = columns.find(({ name }) => argValue === name); + const val = matchedCol ? maybeQuoteValue(matchedCol.name) : argValue; + const mathValue = getFormObject(val); + return { ...mathValue, column: mathValue.column || '' }; + } catch (e) { + return { error: e.message }; + } +}; +// TODO: Garbage, we could make a much nicer math form that can handle way more. +const DatacolumnArgInput = ({ + onValueChange, + columns, + argValue, + renderError, + argId, + typeInstance, +}) => { + const [mathValue, setMathValue] = useState(getMathValue(argValue, columns)); + + useEffect(() => { + setMathValue(getMathValue(argValue, columns)); + }, [argValue, columns]); + + const allowedTypes = typeInstance.options.allowedTypes || false; + const onlyShowMathFunctions = typeInstance.options.onlyMath || false; + + const updateFunctionValue = useCallback( + (fn, column) => { // if setting size, auto-select the first column if no column is already set if (fn === 'size') { - const col = column || (columns[0] && columns[0].name); + const col = column || (columns[0] && columns[0].name) || ''; if (col) { return onValueChange(`${fn}(${maybeQuoteValue(col)})`); } } - // this.inputRefs.column is the column selection, if there is no value, do nothing + // if there is no column value, do nothing if (valueNotSet(column)) { - return setMathFunction(fn); + return setMathValue({ ...mathValue, fn }); } - // this.inputRefs.fn is the math function to use, if it's not set, just use the value input + // if fn is not set, just use the value input if (valueNotSet(fn)) { return onValueChange(column); } - // this.inputRefs.fn has a value, so use it as a math.js expression + // fn has a value, so use it as a math.js expression onValueChange(`${fn}(${maybeQuoteValue(column)})`); - }; - - const column = - columns.map((col) => col.name).find((colName) => colName === mathValue.column) || ''; - - const options = [{ value: '', text: 'select column', disabled: true }]; - - sortBy(columns, 'name').forEach((column) => { - if (allowedTypes && !allowedTypes.includes(column.type)) { - return; - } - options.push({ value: column.name, text: column.name }); - }); - - return ( - - - (this.inputRefs.fn = ref)} - onlymath={onlyShowMathFunctions} - onChange={updateFunctionValue} - /> - - - (this.inputRefs.column = ref)} - onChange={updateFunctionValue} - /> - - - ); + }, + [mathValue, onValueChange, columns] + ); + + const onChangeFn = useCallback( + ({ target: { value } }) => updateFunctionValue(value, mathValue.column), + [mathValue.column, updateFunctionValue] + ); + + const onChangeColumn = useCallback( + ({ target: { value } }) => updateFunctionValue(mathValue.fn, value), + [mathValue.fn, updateFunctionValue] + ); + + if (mathValue.error) { + renderError(); + return null; } -} -const EnhancedDatacolumnArgInput = compose( - withPropsOnChange(['argValue', 'columns'], ({ argValue, columns }) => ({ - mathValue: ((argValue) => { - if (getType(argValue) !== 'string') { - return { error: 'argValue is not a string type' }; - } - try { - const matchedCol = columns.find(({ name }) => argValue === name); - const val = matchedCol ? maybeQuoteValue(matchedCol.name) : argValue; - return getFormObject(val); - } catch (e) { - return { error: e.message }; - } - })(argValue), - })), - createStatefulPropHoc('mathValue', 'setMathValue'), - withHandlers({ - setMathFunction: ({ mathValue, setMathValue }) => (fn) => setMathValue({ ...mathValue, fn }), - }) -)(DatacolumnArgInput); - -EnhancedDatacolumnArgInput.propTypes = { - argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, + const firstColumnOption = { value: '', text: 'select column', disabled: true }; + const options = sortBy(columns, 'name') + .filter((column) => !allowedTypes || allowedTypes.includes(column.type)) + .map(({ name }) => ({ value: name, text: name })); + + return ( + + + + + + + + + ); +}; + +DatacolumnArgInput.propTypes = { columns: PropTypes.array.isRequired, + onValueChange: PropTypes.func.isRequired, + typeInstance: PropTypes.object.isRequired, + renderError: PropTypes.func.isRequired, + argId: PropTypes.string.isRequired, + argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, }; export const datacolumn = () => ({ @@ -148,5 +134,5 @@ export const datacolumn = () => ({ displayName: strings.getDisplayName(), help: strings.getHelp(), default: '""', - simpleTemplate: templateFromReactComponent(EnhancedDatacolumnArgInput), + simpleTemplate: templateFromReactComponent(DatacolumnArgInput), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js index c54b44d3cc5b..5fd859a789d8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js @@ -12,7 +12,7 @@ import { ArgumentStrings } from '../../../../i18n'; const { DataColumn: strings } = ArgumentStrings; -export const SimpleMathFunction = ({ onChange, value, inputRef, onlymath }) => { +export const SimpleMathFunction = ({ onChange, value, onlymath }) => { const options = [ { text: strings.getOptionAverage(), value: 'mean' }, { text: strings.getOptionCount(), value: 'size' }, @@ -29,15 +29,12 @@ export const SimpleMathFunction = ({ onChange, value, inputRef, onlymath }) => { options.unshift({ text: strings.getOptionValue(), value: '' }); } - return ( - - ); + return ; }; SimpleMathFunction.propTypes = { onChange: PropTypes.func, value: PropTypes.string, - inputRef: PropTypes.func, onlymath: PropTypes.bool, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx similarity index 72% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx index 986b39f8ea2e..ef5eb18b4459 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import { compose, withProps } from 'recompose'; +import React from 'react'; import moment from 'moment'; -import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; +import { Assign } from '@kbn/utility-types'; +import { DateFormatArgInput, Props as ComponentProps } from './date_format'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; @@ -16,6 +17,10 @@ import { SetupInitializer } from '../../../plugin'; const { DateFormat: strings } = ArgumentStrings; +const getDateFormatArgInput = (defaultDateFormats: ComponentProps['dateFormats']) => ( + props: Assign +) => ; + export const dateFormatInitializer: SetupInitializer> = ( core, plugins @@ -35,12 +40,10 @@ export const dateFormatInitializer: SetupInitializer(withProps({ dateFormats }))(Component); - return () => ({ name: 'dateFormat', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(DateFormatArgInput), + simpleTemplate: templateFromReactComponent(getDateFormatArgInput(dateFormats)), }); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx similarity index 77% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx index 7c7d573bcd76..e1d86ba3ff9d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { compose, withProps } from 'recompose'; -import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; +import React from 'react'; +import { Assign } from '@kbn/utility-types'; +import { NumberFormatArgInput, Props as ComponentProps } from './number_format'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; @@ -15,6 +16,10 @@ import { FORMATS_UI_SETTINGS } from '../../../../../../../src/plugins/field_form const { NumberFormat: strings } = ArgumentStrings; +const getNumberFormatArgInput = (defaultNumberFormats: ComponentProps['numberFormats']) => ( + props: Assign +) => ; + export const numberFormatInitializer: SetupInitializer> = ( core, plugins @@ -35,14 +40,10 @@ export const numberFormatInitializer: SetupInitializer(withProps({ numberFormats }))( - Component - ); - return () => ({ name: 'numberFormat', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberFormatArgInput), + simpleTemplate: templateFromReactComponent(getNumberFormatArgInput(numberFormats)), }); }; diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js index 88cb529fcdb8..0f22307c9cd1 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js @@ -5,9 +5,8 @@ * 2.0. */ -import React, { PureComponent } from 'react'; +import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { compose, branch, renderComponent } from 'recompose'; import { ErrorBoundary } from '../enhance/error_boundary'; import { ArgSimpleForm } from './arg_simple_form'; import { ArgTemplateForm } from './arg_template_form'; @@ -16,132 +15,131 @@ import { AdvancedFailure } from './advanced_failure'; import { ArgLabel } from './arg_label'; import { PendingArgValue } from './pending_arg_value'; -const branches = [ - // rendered argType args should be resolved, but are not - branch(({ argTypeInstance, resolvedArgValue }) => { - const { argType } = argTypeInstance; +const isPending = (argTypeInstance, resolvedArgValue) => { + const { argType } = argTypeInstance; - // arg does not need to be resolved, no need to branch - if (!argType.resolveArgValue) { - return false; - } + // arg does not need to be resolved, no need to branch + if (!argType.resolveArgValue) { + return false; + } - // arg needs to be resolved, render pending if the value is not defined - return typeof resolvedArgValue === 'undefined'; - }, renderComponent(PendingArgValue)), -]; + // arg needs to be resolved, render pending if the value is not defined + return typeof resolvedArgValue === 'undefined'; +}; // This is what is being generated by render() from the Arg class. It is called in FunctionForm -class ArgFormComponent extends PureComponent { - componentDidMount() { - // keep track of whether or not the component is mounted, to prevent rogue setState calls - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; +export const ArgForm = (props) => { + const { + argId, + argTypeInstance, + templateProps, + valueMissing, + label, + setLabel, + onValueRemove, + workpad, + assets, + renderError, + setRenderError, + resolvedArgValue, + } = props; + + const isMounted = useRef(); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (isPending(argTypeInstance, resolvedArgValue)) { + return ; } - render() { - const { - argId, - argTypeInstance, - templateProps, - valueMissing, - label, - setLabel, - onValueRemove, - workpad, - assets, - renderError, - setRenderError, - resolvedArgValue, - } = this.props; - - return ( - - {({ error, resetErrorState }) => { - const { template, simpleTemplate } = argTypeInstance.argType; - const hasError = Boolean(error) || renderError; - - const argumentProps = { - ...templateProps, - resolvedArgValue, - defaultValue: argTypeInstance.default, - - renderError: () => { - // TODO: don't do this - // It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick - // This is important; Otherwise we end up updating state in the middle of a render cycle - Promise.resolve().then(() => { - // Provide templates with a renderError method, and wrap the error in a known error type - // to stop Kibana's window.error from being called - // see window_error_handler.js for details, - this._isMounted && setRenderError(true); - }); - }, - error: hasError, - setLabel: (label) => this._isMounted && setLabel(label), - resetErrorState: () => { - resetErrorState(); - this._isMounted && setRenderError(false); - }, - label, - workpad, - argId, - assets, - }; - - const expandableLabel = Boolean(hasError || template); - - const simpleArg = ( - + {({ error, resetErrorState }) => { + const { template, simpleTemplate } = argTypeInstance.argType; + const hasError = Boolean(error) || renderError; + + const argumentProps = { + ...templateProps, + resolvedArgValue, + defaultValue: argTypeInstance.default, + + renderError: () => { + // TODO: don't do this + // It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick + // This is important; Otherwise we end up updating state in the middle of a render cycle + Promise.resolve().then(() => { + // Provide templates with a renderError method, and wrap the error in a known error type + // to stop Kibana's window.error from being called + // see window_error_handler.js for details, + isMounted.current && setRenderError(true); + }); + }, + error: hasError, + setLabel: (label) => isMounted.current && setLabel(label), + resetErrorState: () => { + resetErrorState(); + isMounted.current && setRenderError(false); + }, + label, + workpad, + argId, + assets, + }; + + const expandableLabel = Boolean(hasError || template); + + const simpleArg = ( + + + + ); + + const extendedArg = ( +
+ +
+ ); + + return ( +
+ - - - ); - - const extendedArg = ( -
- -
- ); - - return ( -
- - {extendedArg} - -
- ); - }} - - ); - } -} + {extendedArg} +
+
+ ); + }} +
+ ); +}; -ArgFormComponent.propTypes = { +ArgForm.propTypes = { argId: PropTypes.string.isRequired, workpad: PropTypes.object.isRequired, argTypeInstance: PropTypes.shape({ @@ -161,5 +159,3 @@ ArgFormComponent.propTypes = { setRenderError: PropTypes.func.isRequired, resolvedArgValue: PropTypes.any, }; - -export const ArgForm = compose(...branches)(ArgFormComponent); diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_label.js b/x-pack/plugins/canvas/public/components/arg_form/arg_label.js index e199019bc27b..58ad44b4cbf8 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_label.js +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_label.js @@ -30,21 +30,19 @@ export const ArgLabel = (props) => {
{children}
) : ( - simpleArg && ( - - - {label} - -
- } - id={argId} - > - {simpleArg} - - ) + + + {label} + + + } + id={argId} + > + {simpleArg || children} + )}
); diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js deleted file mode 100644 index 4b968c9dd4ee..000000000000 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { compose, withPropsOnChange, withProps } from 'recompose'; -import { RenderToDom } from '../render_to_dom'; -import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; - -class ArgTemplateFormComponent extends React.Component { - static propTypes = { - template: PropTypes.func, - argumentProps: PropTypes.shape({ - valueMissing: PropTypes.bool, - label: PropTypes.string, - setLabel: PropTypes.func.isRequired, - expand: PropTypes.bool, - setExpand: PropTypes.func, - onValueRemove: PropTypes.func, - resetErrorState: PropTypes.func.isRequired, - renderError: PropTypes.func.isRequired, - }), - handlers: PropTypes.object.isRequired, - error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired, - errorTemplate: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, - }; - - UNSAFE_componentWillUpdate(prevProps) { - //see if error state changed - if (this.props.error !== prevProps.error) { - this.props.handlers.destroy(); - } - } - componentDidUpdate() { - if (this.props.error) { - return this._renderErrorTemplate(); - } - this._renderTemplate(this._domNode); - } - - componentWillUnmount() { - this.props.handlers.destroy(); - } - - _domNode = null; - - _renderTemplate = (domNode) => { - const { template, argumentProps, handlers } = this.props; - if (template) { - return template(domNode, argumentProps, handlers); - } - }; - - _renderErrorTemplate = () => { - const { errorTemplate, argumentProps } = this.props; - return React.createElement(errorTemplate, argumentProps); - }; - - render() { - const { template, error } = this.props; - - if (error) { - return this._renderErrorTemplate(); - } - - if (!template) { - return null; - } - - return ( - { - this._domNode = domNode; - this._renderTemplate(domNode); - }} - /> - ); - } -} - -export const ArgTemplateForm = compose( - withPropsOnChange( - () => false, - () => ({ - expressionFormHandlers: new ExpressionFormHandlers(), - }) - ), - withProps(({ handlers, expressionFormHandlers }) => ({ - handlers: Object.assign(expressionFormHandlers, handlers), - })) -)(ArgTemplateFormComponent); diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx new file mode 100644 index 000000000000..1d7227d65e53 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useState, useEffect, useCallback, useRef } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import { RenderToDom } from '../render_to_dom'; +import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; + +interface ArgTemplateFormProps { + template?: ( + domNode: HTMLElement, + config: ArgTemplateFormProps['argumentProps'], + handlers: ArgTemplateFormProps['handlers'] + ) => void; + argumentProps: { + valueMissing?: boolean; + label?: string; + setLabel: (label: string) => void; + expand?: boolean; + setExpand?: (expand: boolean) => void; + onValueRemove?: (argName: string, argIndex: string) => void; + resetErrorState: () => void; + renderError: () => void; + }; + handlers?: { [key: string]: (...args: any[]) => any }; + error?: unknown; + errorTemplate: React.FunctionComponent; +} + +const mergeWithFormHandlers = (handlers: ArgTemplateFormProps['handlers']) => + Object.assign(new ExpressionFormHandlers(), handlers); + +export const ArgTemplateForm: React.FunctionComponent = ({ + template, + argumentProps, + handlers, + error, + errorTemplate, +}) => { + const [updatedHandlers, setHandlers] = useState(mergeWithFormHandlers(handlers)); + const previousError = usePrevious(error); + const domNodeRef = useRef(); + const renderTemplate = useCallback( + (domNode) => template && template(domNode, argumentProps, updatedHandlers), + [template, argumentProps, updatedHandlers] + ); + + const renderErrorTemplate = useCallback(() => React.createElement(errorTemplate, argumentProps), [ + errorTemplate, + argumentProps, + ]); + + useEffect(() => { + setHandlers(mergeWithFormHandlers(handlers)); + }, [handlers]); + + useEffect(() => { + if (previousError !== error) { + updatedHandlers.destroy(); + } + }, [previousError, error, updatedHandlers]); + + useEffect(() => { + if (!error) { + renderTemplate(domNodeRef.current); + } + }, [error, renderTemplate, domNodeRef]); + + if (error) { + return renderErrorTemplate(); + } + + if (!template) { + return null; + } + + return ( + { + domNodeRef.current = domNode; + renderTemplate(domNode); + }} + /> + ); +}; diff --git a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js index f933230f3992..38b29c7d8009 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js +++ b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js @@ -47,7 +47,6 @@ export class PendingArgValue extends React.PureComponent { render() { const { label, argTypeInstance } = this.props; - return (
{ provideHover: this.providerHover, }} options={{ - fontSize: isCompact ? 12 : 16, + fontSize: isCompact ? 12 : 14, scrollBeyondLastLine: false, quickSuggestions: true, minimap: { diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 384a7716657f..87fabe2730c1 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 493fe6430efa..99622df805ce 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index 46e81aa7fa08..d1ff8c63e84c 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json index e0923553bead..4c1f3b20fa23 100644 --- a/x-pack/plugins/cross_cluster_replication/tsconfig.json +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index 567c390edfa5..cebf306003ab 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/dashboard_mode/tsconfig.json b/x-pack/plugins/dashboard_mode/tsconfig.json index 6e4ed11ffa7f..8094e70e96b6 100644 --- a/x-pack/plugins/dashboard_mode/tsconfig.json +++ b/x-pack/plugins/dashboard_mode/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index a0489ecd30aa..da83ded471d0 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -1,3 +1,4 @@ + { "id": "dataEnhanced", "version": "8.0.0", @@ -7,5 +8,10 @@ "optionalPlugins": ["kibanaUtils", "usageCollection", "security"], "server": true, "ui": true, - "requiredBundles": ["kibanaUtils", "kibanaReact"] + "requiredBundles": ["kibanaUtils", "kibanaReact"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Enhanced data plugin. (See src/plugins/data.) Enhances the main data plugin with a search session management UI. Includes a reusable search session indicator component to use in other applications. Exposes routes for managing search sessions. Includes a service that monitors, updates, and cleans up search session saved objects." } diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index 047b9b06516b..544b50c21224 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx index 34faed01f613..b8df4defa18a 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -5,19 +5,24 @@ * 2.0. */ -import React, { FC, useMemo } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { Axis, BarSeries, + BrushEndListener, Chart, + ElementClickListener, niceTimeFormatter, Position, ScaleType, Settings, + XYChartElementEvent, } from '@elastic/charts'; +import moment from 'moment'; +import { useDataVisualizerKibana } from '../../../../kibana_context'; export interface DocumentCountChartPoint { time: number | string; @@ -41,6 +46,10 @@ export const DocumentCountChart: FC = ({ timeRangeLatest, interval, }) => { + const { + services: { data }, + } = useDataVisualizerKibana(); + const seriesName = i18n.translate( 'xpack.dataVisualizer.dataGrid.field.documentCountChart.seriesLabel', { @@ -71,6 +80,35 @@ export const DocumentCountChart: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartPoints, timeRangeEarliest, timeRangeLatest, interval]); + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBrushEnd: BrushEndListener = ({ x }) => { + if (!x) { + return; + } + const [from, to] = x; + timefilterUpdateHandler({ from, to }); + }; + + const onElementClick: ElementClickListener = ([elementData]) => { + const startRange = (elementData as XYChartElementEvent)[0].x; + + const range = { + from: startRange, + to: startRange + interval, + }; + timefilterUpdateHandler(range); + }; + return (
= ({ height: 120, }} > - + = (dataVi from: globalState.time.from, to: globalState.time.to, }); + setLastRefresh(Date.now()); } }, [globalState, timefilter]); useEffect(() => { if (globalState?.refreshInterval !== undefined) { timefilter.setRefreshInterval(globalState.refreshInterval); + setLastRefresh(Date.now()); } }, [globalState, timefilter]); diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7c60002c3c63..ee5f894305d5 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 38a55e557909..f372b7a7539a 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 50fe41c49b0c..85a215254bb7 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index de8317f3f7b8..65904ab5826f 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { DynamicActionsState, UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, @@ -15,12 +16,11 @@ import { EmbeddableOutput, IEmbeddable, } from '../../../../../src/plugins/embeddable/public'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; export interface EmbeddableWithDynamicActionsInput extends EmbeddableInput { enhancements?: { dynamicActions: DynamicActionsState; - [key: string]: SerializableState; + [key: string]: SerializableRecord; }; } diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index 6e9eb69585cb..f9ac36958747 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 2b51b313d34f..4b06756a9cf2 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx new file mode 100644 index 000000000000..21a76734fb33 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 '../../../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldNumber, + EuiForm, + EuiSelect, + EuiSwitch, +} from '@elastic/eui'; + +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; + +const MOCK_ACTIONS = { + // AutomaticCrawlSchedulerLogic + fetchCrawlSchedule: jest.fn(), + setCrawlFrequency: jest.fn(), + setCrawlUnit: jest.fn(), + saveChanges: jest.fn(), + toggleCrawlAutomatically: jest.fn(), + // ManageCrawlsPopoverLogic + closePopover: jest.fn(), +}; + +const MOCK_VALUES = { + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + isSubmitting: false, +}; + +describe('AutomaticCrawlScheduler', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + + wrapper = shallow(); + }); + + it('calls fetchCrawlSchedule on component load', () => { + expect(MOCK_ACTIONS.fetchCrawlSchedule).toHaveBeenCalled(); + }); + + it('renders', () => { + expect(wrapper.find(EuiForm)).toHaveLength(1); + expect(wrapper.find(EuiFieldNumber)).toHaveLength(1); + expect(wrapper.find(EuiSelect)).toHaveLength(1); + }); + + it('saves changes on form submit', () => { + const preventDefault = jest.fn(); + wrapper.find(EuiForm).simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(MOCK_ACTIONS.saveChanges).toHaveBeenCalled(); + }); + + it('contains a switch that toggles automatic crawling', () => { + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.toggleCrawlAutomatically).toHaveBeenCalled(); + }); + + it('contains a number field that updates the crawl frequency', () => { + wrapper.find(EuiFieldNumber).simulate('change', { target: { value: '10' } }); + + expect(MOCK_ACTIONS.setCrawlFrequency).toHaveBeenCalledWith(10); + }); + + it('contains a select field that updates the crawl unit', () => { + wrapper.find(EuiSelect).simulate('change', { target: { value: CrawlUnits.weeks } }); + + expect(MOCK_ACTIONS.setCrawlUnit).toHaveBeenCalledWith(CrawlUnits.weeks); + }); + + it('contains a button to close the popover', () => { + expect(wrapper.find(EuiButtonEmpty).prop('onClick')).toEqual(MOCK_ACTIONS.closePopover); + }); + + it('contains a submit button', () => { + expect(wrapper.find(EuiButton).prop('type')).toEqual('submit'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx new file mode 100644 index 000000000000..b0ec31d2ff64 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx @@ -0,0 +1,204 @@ +/* + * 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 } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiPopoverFooter, + EuiSelect, + EuiSpacer, + EuiSwitch, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + DAYS_UNIT_LABEL, + HOURS_UNIT_LABEL, + MONTHS_UNIT_LABEL, + WEEKS_UNIT_LABEL, +} from '../../../../..//shared/constants/units'; +import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { DOCS_PREFIX } from '../../../../routes'; +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; + +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +export const AutomaticCrawlScheduler: React.FC = () => { + const { + fetchCrawlSchedule, + setCrawlFrequency, + setCrawlUnit, + saveChanges, + toggleCrawlAutomatically, + } = useActions(AutomaticCrawlSchedulerLogic); + + const { closePopover } = useActions(ManageCrawlsPopoverLogic); + + const { crawlAutomatically, crawlFrequency, crawlUnit, isSubmitting } = useValues( + AutomaticCrawlSchedulerLogic + ); + + useEffect(() => { + fetchCrawlSchedule(); + }, []); + + const formId = htmlIdGenerator('AutomaticCrawlScheduler')(); + + return ( + { + event.preventDefault(); + saveChanges(); + }} + component="form" + id={formId} + > + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.readMoreLink', + { + defaultMessage: 'Read more.', + } + )} + + ), + }} + /> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel', + { + defaultMessage: 'Crawl automatically', + } + )} + + } + onChange={toggleCrawlAutomatically} + compressed + /> + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix', + { + defaultMessage: 'Every', + } + )} + + + + setCrawlFrequency(parseInt(e.target.value, 10))} + /> + + + setCrawlUnit(e.target.value as CrawlUnits)} + /> + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.scheduleDescription', + { + defaultMessage: 'The crawl schedule applies to every domain on this engine.', + } + )} + + + + + + {CANCEL_BUTTON_LABEL} + + + + {SAVE_BUTTON_LABEL} + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts new file mode 100644 index 000000000000..3c95243459a1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts @@ -0,0 +1,307 @@ +/* + * 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, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/engine_logic.mock'; + +jest.mock('./manage_crawls_popover_logic', () => ({ + ManageCrawlsPopoverLogic: { + actions: { + closePopover: jest.fn(), + }, + }, +})); + +import { nextTick } from '@kbn/test/jest'; + +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +describe('AutomaticCrawlSchedulerLogic', () => { + const { mount } = new LogicMounter(AutomaticCrawlSchedulerLogic); + const { http } = mockHttpValues; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + + expect(AutomaticCrawlSchedulerLogic.values).toEqual({ + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + isSubmitting: false, + }); + }); + + describe('actions', () => { + describe('clearCrawlSchedule', () => { + it('sets crawl schedule related values to their defaults', () => { + mount({ + crawlAutomatically: true, + crawlFrequency: 36, + crawlUnit: CrawlUnits.hours, + }); + + AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule(); + + expect(AutomaticCrawlSchedulerLogic.values).toMatchObject({ + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + }); + }); + }); + + describe('toggleCrawlAutomatically', () => { + it('toggles the ability to crawl automatically', () => { + mount({ + crawlAutomatically: false, + }); + + AutomaticCrawlSchedulerLogic.actions.toggleCrawlAutomatically(); + + expect(AutomaticCrawlSchedulerLogic.values.crawlAutomatically).toEqual(true); + + AutomaticCrawlSchedulerLogic.actions.toggleCrawlAutomatically(); + + expect(AutomaticCrawlSchedulerLogic.values.crawlAutomatically).toEqual(false); + }); + }); + + describe('onDoneSubmitting', () => { + mount({ + isSubmitting: true, + }); + + AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting(); + + expect(AutomaticCrawlSchedulerLogic.values.isSubmitting).toEqual(false); + }); + + describe('setCrawlFrequency', () => { + it("sets the crawl schedule's frequency", () => { + mount({ + crawlFrequency: 36, + }); + + AutomaticCrawlSchedulerLogic.actions.setCrawlFrequency(12); + + expect(AutomaticCrawlSchedulerLogic.values.crawlFrequency).toEqual(12); + }); + }); + + describe('setCrawlSchedule', () => { + it("sets the crawl schedule's frequency and unit, and enables crawling automatically", () => { + mount(); + + AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule({ + frequency: 3, + unit: CrawlUnits.hours, + }); + + expect(AutomaticCrawlSchedulerLogic.values).toMatchObject({ + crawlAutomatically: true, + crawlFrequency: 3, + crawlUnit: CrawlUnits.hours, + }); + }); + }); + + describe('setCrawlUnit', () => { + it("sets the crawl schedule's unit", () => { + mount({ + crawlUnit: CrawlUnits.months, + }); + + AutomaticCrawlSchedulerLogic.actions.setCrawlUnit(CrawlUnits.weeks); + + expect(AutomaticCrawlSchedulerLogic.values.crawlUnit).toEqual(CrawlUnits.weeks); + }); + }); + }); + + describe('listeners', () => { + describe('deleteCrawlSchedule', () => { + it('resets the states of the crawl scheduler and popover, and shows a toast, on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + jest.spyOn(ManageCrawlsPopoverLogic.actions, 'closePopover'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce(Promise.resolve()); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + describe('error paths', () => { + it('resets the states of the crawl scheduler and popover on a 404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + jest.spyOn(ManageCrawlsPopoverLogic.actions, 'closePopover'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 404 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + it('flashes an error on a non-404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + }); + }); + + describe('fetchCrawlSchedule', () => { + it('set the state of the crawl scheduler on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'setCrawlSchedule'); + http.get.mockReturnValueOnce( + Promise.resolve({ + unit: CrawlUnits.days, + frequency: '30', + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule).toHaveBeenCalledWith({ + unit: CrawlUnits.days, + frequency: '30', + }); + }); + + describe('error paths', () => { + it('resets the states of the crawl scheduler on a 404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + http.get.mockReturnValueOnce( + Promise.reject({ + response: { status: 404 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + }); + + it('flashes an error on a non-404 respose', async () => { + http.get.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + }); + }); + }); + + describe('saveChanges', () => { + it('updates or creates a crawl schedule if the user has chosen to crawl automatically', () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'submitCrawlSchedule'); + mount({ + crawlAutomatically: true, + }); + + AutomaticCrawlSchedulerLogic.actions.saveChanges(); + + expect(AutomaticCrawlSchedulerLogic.actions.submitCrawlSchedule); + }); + + it('deletes the crawl schedule if the user has chosen to disable automatic crawling', () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'deleteCrawlSchedule'); + mount({ + crawlAutomatically: false, + }); + + AutomaticCrawlSchedulerLogic.actions.saveChanges(); + + expect(AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule); + }); + }); + + describe('submitCrawlSchedule', () => { + it('sets the states of the crawl scheduler and closes the popover on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'setCrawlSchedule'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.put.mockReturnValueOnce( + Promise.resolve({ + unit: CrawlUnits.days, + frequency: 30, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.submitCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule).toHaveBeenCalledWith({ + unit: CrawlUnits.days, + frequency: 30, + }); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + it('flashes an error callout if there is an error', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts new file mode 100644 index 000000000000..75c403fa3588 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts @@ -0,0 +1,189 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { EngineLogic } from '../../../engine'; +import { CrawlSchedule, CrawlUnits } from '../../types'; + +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +export interface AutomaticCrawlSchedulerLogicValues { + crawlAutomatically: boolean; + crawlFrequency: CrawlSchedule['frequency']; + crawlUnit: CrawlSchedule['unit']; + isSubmitting: boolean; +} + +const DEFAULT_VALUES: Pick = { + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, +}; + +export interface AutomaticCrawlSchedulerLogicActions { + clearCrawlSchedule(): void; + deleteCrawlSchedule(): void; + disableCrawlAutomatically(): void; + onDoneSubmitting(): void; + enableCrawlAutomatically(): void; + fetchCrawlSchedule(): void; + saveChanges(): void; + setCrawlFrequency( + crawlFrequency: CrawlSchedule['frequency'] + ): { crawlFrequency: CrawlSchedule['frequency'] }; + setCrawlSchedule(crawlSchedule: CrawlSchedule): { crawlSchedule: CrawlSchedule }; + setCrawlUnit(crawlUnit: CrawlSchedule['unit']): { crawlUnit: CrawlSchedule['unit'] }; + submitCrawlSchedule(): void; + toggleCrawlAutomatically(): void; +} + +export const AutomaticCrawlSchedulerLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'automatic_crawl_scheduler'], + actions: () => ({ + clearCrawlSchedule: true, + deleteCrawlSchedule: true, + disableCrawlAutomatically: true, + onDoneSubmitting: true, + enableCrawlAutomatically: true, + fetchCrawlSchedule: true, + saveChanges: true, + setCrawlSchedule: (crawlSchedule: CrawlSchedule) => ({ crawlSchedule }), + submitCrawlSchedule: true, + setCrawlFrequency: (crawlFrequency: string) => ({ crawlFrequency }), + setCrawlUnit: (crawlUnit: CrawlUnits) => ({ crawlUnit }), + toggleCrawlAutomatically: true, + }), + reducers: () => ({ + crawlAutomatically: [ + false, + { + clearCrawlSchedule: () => false, + setCrawlSchedule: () => true, + toggleCrawlAutomatically: (crawlAutomatically) => !crawlAutomatically, + }, + ], + crawlFrequency: [ + DEFAULT_VALUES.crawlFrequency, + { + clearCrawlSchedule: () => DEFAULT_VALUES.crawlFrequency, + setCrawlSchedule: (_, { crawlSchedule: { frequency } }) => frequency, + setCrawlFrequency: (_, { crawlFrequency }) => crawlFrequency, + }, + ], + crawlUnit: [ + DEFAULT_VALUES.crawlUnit, + { + clearCrawlSchedule: () => DEFAULT_VALUES.crawlUnit, + setCrawlSchedule: (_, { crawlSchedule: { unit } }) => unit, + setCrawlUnit: (_, { crawlUnit }) => crawlUnit, + }, + ], + isSubmitting: [ + false, + { + deleteCrawlSchedule: () => true, + onDoneSubmitting: () => false, + submitCrawlSchedule: () => true, + }, + ], + }), + listeners: ({ actions, values }) => ({ + deleteCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { closePopover } = ManageCrawlsPopoverLogic.actions; + + try { + await http.delete(`/api/app_search/engines/${engineName}/crawler/crawl_schedule`); + actions.clearCrawlSchedule(); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage', + { + defaultMessage: 'Automatic crawling has been disabled.', + } + ) + ); + closePopover(); + } catch (e) { + // A 404 is expected and means the user has no crawl schedule to delete + if (e.response?.status === 404) { + actions.clearCrawlSchedule(); + closePopover(); + } else { + flashAPIErrors(e); + // Keep the popover open + } + } finally { + actions.onDoneSubmitting(); + } + }, + fetchCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const crawlSchedule: CrawlSchedule = await http.get( + `/api/app_search/engines/${engineName}/crawler/crawl_schedule` + ); + actions.setCrawlSchedule(crawlSchedule); + } catch (e) { + // A 404 is expected and means the user does not have crawl schedule + // for this engine. We continue to use the defaults. + if (e.response.status === 404) { + actions.clearCrawlSchedule(); + } else { + flashAPIErrors(e); + } + } + }, + saveChanges: () => { + if (values.crawlAutomatically) { + actions.submitCrawlSchedule(); + } else { + actions.deleteCrawlSchedule(); + } + }, + submitCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { closePopover } = ManageCrawlsPopoverLogic.actions; + + try { + const crawlSchedule: CrawlSchedule = await http.put( + `/api/app_search/engines/${engineName}/crawler/crawl_schedule`, + { + body: JSON.stringify({ + unit: values.crawlUnit, + frequency: values.crawlFrequency, + }), + } + ); + actions.setCrawlSchedule(crawlSchedule); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage', + { + defaultMessage: 'Your automatic crawling schedule has been updated.', + } + ) + ); + closePopover(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.onDoneSubmitting(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx index e97d9a703b66..db6063c2d6bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx @@ -9,7 +9,7 @@ import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logi import React from 'react'; -import { shallow } from 'enzyme'; +import { ReactWrapper, shallow } from 'enzyme'; import { EuiButton, @@ -22,6 +22,7 @@ import { import { mountWithIntl } from '../../../../../test_helpers'; import { CrawlerDomain } from '../../types'; +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; import { ManageCrawlsPopover } from './manage_crawls_popover'; const MOCK_ACTIONS = { @@ -57,22 +58,33 @@ describe('ManageCrawlsPopover', () => { expect(wrapper.find(EuiContextMenuPanel)).toHaveLength(0); }); - it('includes a context menu when open', () => { - setMockValues({ - ...MOCK_VALUES, - isOpen: true, - }); + describe('when open', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + setMockValues({ + ...MOCK_VALUES, + isOpen: true, + }); + + wrapper = mountWithIntl(); - const wrapper = mountWithIntl(); + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); - const menuItems = wrapper - .find(EuiContextMenuPanel) - .find(EuiResizeObserver) - .find(EuiContextMenuItem); + it('includes a button to reapply crawl rules', () => { + menuItems.at(0).simulate('click'); + expect(MOCK_ACTIONS.reApplyCrawlRules).toHaveBeenCalledWith(MOCK_DOMAIN); + }); - expect(menuItems).toHaveLength(1); + it('includes a form to set a crawl schedule ', () => { + menuItems.at(1).simulate('click'); - menuItems.first().simulate('click'); - expect(MOCK_ACTIONS.reApplyCrawlRules).toHaveBeenCalledWith(MOCK_DOMAIN); + expect(wrapper.find(EuiContextMenuPanel).find(AutomaticCrawlScheduler)); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx index a980ee73ebcd..f76c79377685 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { CrawlerDomain } from '../../types'; -// import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; @@ -40,19 +40,25 @@ export const ManageCrawlsPopover: React.FC = ({ domain icon: 'refresh', onClick: () => reApplyCrawlRules(domain), }, - // { - // name: 'Automatic Crawling', - // icon: 'gear', - // panel: 1, - // }, + { + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.automaticCrawlingButtonLabel', + { defaultMessage: 'Automatic crawling' } + ), + icon: 'gear', + panel: 1, + }, ], }, - // { - // id: 1, - // title: 'Automatic Crawling', - // width: 400, - // content: , - // }, + { + id: 1, + title: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.automaticCrawlingTitle', + { defaultMessage: 'Automatic crawling' } + ), + width: 400, + content: , + }, ]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts index a804164a90dc..f80465fa2c40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts @@ -76,7 +76,7 @@ describe('ManageCrawlsPopoverLogic', () => { } as CrawlerDomain); await nextTick(); - expect(flashSuccessToast).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts index c269a2ab595c..50d228ed2fc7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts @@ -7,6 +7,8 @@ import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { EngineLogic } from '../../../engine'; @@ -55,7 +57,14 @@ export const ManageCrawlsPopoverLogic = kea< body: JSON.stringify(requestBody), }); - flashSuccessToast('Crawl Rules are being re-applied in the background'); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.reApplyCrawlRules.successMessage', + { + defaultMessage: 'Crawl rules are being re-applied in the background', + } + ) + ); } catch (e) { flashAPIErrors(e); } finally { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index 52e3ca19e69e..903068e28c39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -57,15 +57,17 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co'); }); - it('uses a placeholder for the page title and page chrome if a domain has not been set', () => { + it('does not render a page header and uses placeholder chrome while loading', () => { setMockValues({ ...MOCK_VALUES, + dataLoading: true, domain: null, }); const wrapper = shallow(); - expect(wrapper.prop('pageHeader').pageTitle).toEqual('Loading...'); + expect(wrapper.prop('pageChrome')).toContain('...'); + expect(wrapper.prop('pageHeader')).toBeUndefined(); }); it('contains a crawler status banner', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index aaa5f0d55398..6419c31cc16c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -32,23 +32,21 @@ export const CrawlerSingleDomain: React.FC = () => { const { fetchDomainData } = useActions(CrawlerSingleDomainLogic); - const displayDomainUrl = domain - ? domain.url - : i18n.translate('xpack.enterpriseSearch.appSearch.crawler.singleDomain.loadingTitle', { - defaultMessage: 'Loading...', - }); - useEffect(() => { fetchDomainData(domainId); }, []); return ( , ], - }} + pageChrome={getEngineBreadcrumbs([CRAWLER_TITLE, domain?.url || '...'])} + pageHeader={ + dataLoading + ? undefined + : { + pageTitle: domain!.url, + rightSideItems: [, ], + } + } isLoading={dataLoading} > diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 8f1abd6cb055..0902499feb4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -175,3 +175,17 @@ export const readableCrawlerStatuses: { [key in CrawlerStatus]: string } = { { defaultMessage: 'Skipped' } ), }; + +export interface CrawlSchedule { + frequency: number; + unit: CrawlUnits; +} + +// The BE uses a singular form of each unit +// See shared_togo/app/models/shared_togo/crawler/crawl_schedule.rb +export enum CrawlUnits { + hours = 'hour', + days = 'day', + weeks = 'week', + months = 'month', +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts index e6511947d250..6579e911cc19 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts @@ -44,3 +44,8 @@ export const CLOSE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.actions.closeButtonLabel', { defaultMessage: 'Close' } ); + +export const RESET_DEFAULT_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.actions.resetDefaultButtonLabel', + { defaultMessage: 'Reset to default' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts new file mode 100644 index 000000000000..9d8c30c7e57d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const HOURS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.hoursLabel', { + defaultMessage: 'Hours', +}); + +export const DAYS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.daysLabel', { + defaultMessage: 'Days', +}); + +export const WEEKS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.weeksLabel', { + defaultMessage: 'Weeks', +}); + +export const MONTHS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.monthsLabel', { + defaultMessage: 'Months', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 583652de1fa0..25a1e084a3a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -219,7 +219,7 @@ export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate( export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.noResults.message', - { defaultMessage: 'Create a new role mapping' } + { defaultMessage: 'No matching role mappings found' } ); export const ROLES_DISABLED_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 61043aa6ad9a..003848d1da8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -10,8 +10,10 @@ import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; -import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui'; +import { EuiInMemoryTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui'; +import type { EuiSearchBarProps } from '@elastic/eui'; import { engines } from '../../app_search/__mocks__/engines.mock'; @@ -106,4 +108,27 @@ describe('RoleMappingsTable', () => { `${engines[0].name}, ${engines[1].name} + 1` ); }); + + it('handles search', () => { + const wrapper = mount( + + ); + const roleMappingsTable = wrapper.find('[data-test-subj="RoleMappingsTable"]').first(); + const searchProp = roleMappingsTable.prop('search') as EuiSearchBarProps; + + act(() => { + if (searchProp.onChange) { + searchProp.onChange({ queryText: 'admin' } as any); + } + }); + wrapper.update(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index a98d36f04b4a..d6299bc1b389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import type { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { WSRoleMapping } from '../../workplace_search/types'; @@ -70,6 +71,8 @@ export const RoleMappingsTable: React.FC = ({ return _rm; }) as SharedRoleMapping[]; + const [items, setItems] = useState(standardizedRoleMappings); + const attributeNameCol: EuiBasicTableColumn = { field: 'attribute', name: ( @@ -161,7 +164,22 @@ export const RoleMappingsTable: React.FC = ({ pageSize: 10, }; + const onQueryChange = ({ queryText }: EuiSearchBarOnChangeArgs) => { + const filteredItems = standardizedRoleMappings.filter((rm) => { + // JSON.stringify allows us to search all the object fields + // without converting all the nested arrays and objects to strings manually + // Some false-positives are possible, because the search is also performed on + // object keys, but the simplicity of JSON.stringify seems to worth the tradeoff. + const normalizedTableItemString = JSON.stringify(rm).toLowerCase(); + const normalizedQuery = queryText.toLowerCase(); + return normalizedTableItemString.indexOf(normalizedQuery) !== -1; + }); + + setItems(filteredItems); + }; + const search = { + onChange: onQueryChange, box: { incremental: true, fullWidth: false, @@ -173,7 +191,7 @@ export const RoleMappingsTable: React.FC = ({ { expect(cell.find(EuiBadge)).toHaveLength(1); }); + + it('handles search', () => { + const wrapper = mount( + + ); + const roleMappingsTable = wrapper.find('[data-test-subj="UsersTable"]').first(); + const searchProp = roleMappingsTable.prop('search') as EuiSearchBarProps; + + act(() => { + if (searchProp.onChange) { + searchProp.onChange({ queryText: 'admin' } as any); + } + }); + wrapper.update(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx index 25a9eee38f93..3b6e2dc440a7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiBadge, EuiBasicTableColumn, EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; +import type { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { SingleUserRoleMapping } from '../../shared/types'; @@ -73,6 +74,8 @@ export const UsersTable: React.FC = ({ invitation: user.invitation, })) as unknown) as Array>; + const [items, setItems] = useState(users); + const columns: Array> = [ { field: 'username', @@ -134,7 +137,22 @@ export const UsersTable: React.FC = ({ pageSize: 10, }; + const onQueryChange = ({ queryText }: EuiSearchBarOnChangeArgs) => { + const filteredItems = users.filter((user) => { + // JSON.stringify allows us to search all the object fields + // without converting all the nested arrays and objects to strings manually + // Some false-positives are possible, because the search is also performed on + // object keys, but the simplicity of JSON.stringify seems to worth the tradeoff. + const normalizedTableItemString = JSON.stringify(user).toLowerCase(); + const normalizedQuery = queryText.toLowerCase(); + return normalizedTableItemString.indexOf(normalizedQuery) !== -1; + }); + + setItems(filteredItems); + }; + const search = { + onChange: onQueryChange, box: { incremental: true, fullWidth: false, @@ -147,7 +165,7 @@ export const UsersTable: React.FC = ({ { + it('reads a file and returns base64 string', async () => { + const file = new File(['a mock file'], 'mockFile.png', { type: 'img/png' }); + const text = await readUploadedFileAsBase64(file); + expect(text).toEqual('YSBtb2NrIGZpbGU='); + }); + + it('throws an error if the file cannot be read', async () => { + const badFile = ('causes an error' as unknown) as File; + await expect(readUploadedFileAsBase64(badFile)).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts new file mode 100644 index 000000000000..d9f6d177cf9c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const readUploadedFileAsBase64 = (fileInput: File): Promise => { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = () => { + // We need to split off the prefix from the DataUrl and only pass the base64 string + // before: 'data:image/png;base64,encodedData==' + // after: 'encodedData==' + const base64 = (reader.result as string).split(',')[1]; + resolve(base64); + }; + try { + reader.readAsDataURL(fileInput); + } catch { + reader.abort(); + reject(new Error()); + } + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx new file mode 100644 index 000000000000..0f96b76130b4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, mount } from 'enzyme'; + +import { EuiFilePicker, EuiConfirmModal } from '@elastic/eui'; +import { nextTick } from '@kbn/test/jest'; + +jest.mock('../../../utils', () => ({ + readUploadedFileAsBase64: jest.fn(({ img }) => img), +})); +import { readUploadedFileAsBase64 } from '../../../utils'; + +import { RESET_IMAGE_TITLE } from '../constants'; + +import { BrandingSection, defaultLogo } from './branding_section'; + +describe('BrandingSection', () => { + const stageImage = jest.fn(); + const saveImage = jest.fn(); + const resetImage = jest.fn(); + + const props = { + image: 'foo', + imageType: 'logo' as 'logo', + description: 'logo test', + helpText: 'this is a logo', + stageImage, + saveImage, + resetImage, + }; + + it('renders logo', () => { + const wrapper = mount(); + + expect(wrapper.find(EuiFilePicker)).toHaveLength(1); + }); + + it('renders icon copy', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="ResetImageButton"]').simulate('click'); + + expect(wrapper.find(EuiConfirmModal).prop('title')).toEqual(RESET_IMAGE_TITLE); + }); + + it('renders default Workplace Search logo', () => { + const wrapper = shallow(); + + expect(wrapper.find('img').prop('src')).toContain(defaultLogo); + }); + + describe('resetConfirmModal', () => { + it('calls method and hides modal when modal confirmed', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="ResetImageButton"]').simulate('click'); + wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(0); + expect(resetImage).toHaveBeenCalled(); + }); + }); + + describe('handleUpload', () => { + it('handles empty files', () => { + const wrapper = shallow(); + wrapper.find(EuiFilePicker).prop('onChange')!([] as any); + + expect(stageImage).toHaveBeenCalledWith(null); + }); + + it('handles image', async () => { + const wrapper = shallow(); + wrapper.find(EuiFilePicker).prop('onChange')!(['foo'] as any); + + expect(readUploadedFileAsBase64).toHaveBeenCalledWith('foo'); + await nextTick(); + expect(stageImage).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx new file mode 100644 index 000000000000..776e72c4026c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx @@ -0,0 +1,152 @@ +/* + * 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, { useState, useEffect } from 'react'; + +import { + EuiButton, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFilePicker, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { + SAVE_BUTTON_LABEL, + CANCEL_BUTTON_LABEL, + RESET_DEFAULT_BUTTON_LABEL, +} from '../../../../shared/constants'; +import { readUploadedFileAsBase64 } from '../../../utils'; + +import { + LOGO_TEXT, + ICON_TEXT, + RESET_IMAGE_TITLE, + RESET_LOGO_DESCRIPTION, + RESET_ICON_DESCRIPTION, + RESET_IMAGE_CONFIRMATION_TEXT, + ORGANIZATION_LABEL, + BRAND_TEXT, +} from '../constants'; + +export const defaultLogo = + 'iVBORw0KGgoAAAANSUhEUgAAAMMAAAAeCAMAAACmAVppAAABp1BMVEUAAAAmLjf/xRPwTpglLjf/xhIlLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjcwMTslLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjclLjf+xBMlLjclLjclLjclLjclLjf/xxBUOFP+wRclLjf+xxb/0w3wTpgkLkP+xRM6ME3wTphKPEnxU5PwT5f/yhDwTpj/xxD/yBJQLF/wTpjyWY7/zQw5I1z/0Aj3SKT/zg//zg38syyoOYfhTZL/0QT+xRP/Uqr/UqtBMFD+xBV6SllaOVY7J1VXM1v/yhH/1wYlLjf+xRPwTpgzN0HvTpc1OEH+xBMuNj7/UaX/UKEXMzQQMzH4TpvwS5swNkArNj4nNTv/UqflTZPdTJA6OEQiNDr/yQ7zT5q9SIB1P19nPlhMOkz/UqbUTIvSS4oFLTD1hLkfAAAAbXRSTlMADfLy4wwCKflGIPzzaF0k8BEFlMd/G9rNFAjosWJWNC8s1LZ4bey9q6SZclHewJxlQDkLoIqDfE09So4Y6MSniIaFy8G8h04Q/vb29ObitpyQiodmXlZUVDssJSQfHQj+7Ovi4caspKFzbGw11xUNcgAABZRJREFUWMPVmIeT0kAUh180IoQOJyAgvQt4dLD33nvvXX8ed/beu3+0bzcJtjiDjuMM38xluU12932b3U2ytGu+ZM8RGrFl0zzJqgU0GczoPHq0l3QWXH79+vYtyaQ4zJ8x2U+C0xtumcybPIeZw/zv8fO3Jtph2wmim7cn2mF29uIZoqO3J9lh5tnnjZxx4PbkOsw+e/H4wVXO2WTpoCgBIyUz/QnrPGopNhoTZWHaT2MTUAI/OczePTt3//Gd60Rb51k5OOyqKLLS56oS03at+zUEl8tCIuNaOKZBxQmgHKIx6bl6PzrM3pt9eX9ueGfuGNENKwc/0OTEAywjxo4q/YwfsHDwIT2eQgaYqgOxxTQea9H50eHhvfcP5obD4ZPdnLfKaj5kkeNjEKhxkoQ9Sj9iI8V0+GHwqBjvPuSQ8RKFwmjTeCzCItPBGElv798ZMo/vHCLaZ+WwFFk+huGE1/wnN6VmPZxGl63QSoUGSYdBOe6n9opWJxzp2UwHW66urs6RIFkJhyspYhZ3Mmq5QQZxTMvT5aV81ILhWrsp+4Mbqef5R7rsaa5WNSJ3US26pcN0qliL902HN3ffPRhKnm4k2mLlkIY9QF6sXga3aDBP/ghgB8pyELkAj3QYgLunBYTBTEV1B60G+CC9+5Bw6Joqy7tJJ4iplaO2fPJUlcyScaIqnAC8lIUgKxyKEFQNh4czH17pDk92RumklQPFMKAlyHtRInJxZW2++baBj2NXfCg0Qq0oQCFgKYkMV7PVLKCnOyxFRqOQCgf5nVgXjQYBogiCAY4MxiT2OuEMeuRkCKjYbOO2nArlENFIK6BJDqCe0riqWDOQ9CHHDugqoSKmDId7z18+HepsV2jrDiuHZRxdiSuDi7yIURTQiLilDNmcSMo5XUipQoEUOxycJKDqDooMrYQ8ublJplKyebkgs54zdZKyh0tp4nCLeoMeo2Qdbs4sEFNAn4+Nspt68iov7H/gkECJfIjSFAIJVGiAmhzUAJHemYrL7uRrxC/wdSQ0zTldDcZjwBJqs6OOG7VyPLsmgjVk4s2XAHuKowvzqXIYK0Ylpw0xDbCN5nRQz/iDseSHmhK9mENiPRJURUTOOenAccoRBKhe3UGeMx1SqpgcGXhoDf/p5MHKTsTUzfQdoSyH2tVPqWqekqJkJMb2DtT5fOo7B7nKLwTGn9NiABdFL7KICj8l4SPjXpoOdiwPIqw7LBYB6Q4aZdDWAtThSIKyb6nlt3kQp+8IrFtk0+vz0TSCZBDGMi5ZGjks1msmxf/NYey1VYrrsarAau5kn+zSCocSNRwAMfPbYlRhhb7UiKtDZIdNxjNNy1GIciQFZ0CB3c+Znm5KdwDkk38dIqQhJkfbIs0GEFMbOVBEPtk69hXfHMZ+xjFNQCUZNnpyNiPn4N9J8o8cFEqLsdtyOVFJBIHlQsrLUyg+6Ef4jIgh7EmEUReGsSWNtYCDJNNAyZ3PAgniEVfzNCqi1gjKzX5Gzge5GnCCYH89MKD1aP/oMHvv+Zz5rnHwd++tPlT0yY2kSLtgfFUZfNp0IDeQIhQWgVlkvGukVQC1Kbj5FqwGU/fLdYdxLSGDHgR2MecDcTCFPlEyBiBT5JLLESGB2wnAyTWtlatB2nSQo+nF8P7cq2tEC+b9ziGVWClv+3KHuY6s9YhgbI7lLZk4xJBpeNIBOGlhN7eQmEFfYT13x00rEyES57vdhlFfrrNkJY0ILel2+QEhSfbWehS57uU707Lk4mrSuMy9Oa+J1hOi41oczMhh5tmLuS9XLN69/wI/0KL/BzuYEh8/XfpH30ByVP0/2GFkceFffYvKL4n/gPWewPF/syeg/B8F672ZU+duTfD3tLlHtur1xDn8sld5Smz0TdZepcWe8cENk7Vn/BXafhbMBIo0xQAAAABJRU5ErkJggg=='; +const defaultIcon = + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAA/1BMVEUAAADwTpj+xRT+xRTwTpjwTpj+xRTwTpj+wxT+xRT/yBMSHkn8yxL+wxX+xRT+xRT/1QwzN0E6OkH/xxPwTpjwTpjwTpj/xBQsMUPwTpj/UK3/yRMWHkvwTpj/zg7wTpj/0A3wTpjwTpgRIEf/0Qx/P2P/yBMuM0I1OEH+xRQuM0L+xRQuM0LntRr+xRT+xRT+xBQ1JlZjPVdaUDwtMEUbJkYbJEj+xRTwTpg0N0E2N0LuTZX/U6z/Uqf9UaFkPVYRMjD/UqnzTpgKMS0BMCn/UaL3T53gTJGwRn2jRHRdPFUtNj4qNjwmNToALyfKSojISoeJQWhtPlsFKTP/yxKq4k7GAAAAN3RSTlMA29vt7fPy6uPQdjYd/aSVBfHs49nPwq+nlIuEU084MichEAoK/vPXz6iempOSjn9kY1w0LBcVaxnnyQAAASFJREFUOMuVk3lbgkAQh6cIxQq0u6zM7vs+cHchRbE7O7//Z+nng60PDuDj+9/MvMCyM0O0YE4Ac35lkzTTp3M5A+QKCPK1HuY69bjY+3UjDERjNc1GVD9zNeNxIb+FeOfYZYJmEXHFzhBUGYnVdEHde1fILHFB1+uNG5zCYoKuh2L2jqhqJwnqwfsOpRQHyE0mCU3vqyOkEOIESYsLyv9svUoB5BRewYVm8NJCvcsymsGF9uP7m4iY2SYqMMF/aoh/8I1DLjz3hTWi4ogC/4Qz9JCj/6byP7IvCle925Fd4yj5qtGsoB7C2I83i7f7Fiew0wfm55qoZKWOXDu4zBo5UMbz50PGvop85uKUigMCXz0nJrDlja2OQcnrX3H0+v8BzVCfXpvPH1sAAAAASUVORK5CYII='; + +interface Props { + imageType: 'logo' | 'icon'; + description: string; + helpText: string; + image?: string | null; + stagedImage?: string | null; + stageImage(image: string | null): void; + saveImage(): void; + resetImage(): void; +} + +export const BrandingSection: React.FC = ({ + imageType, + description, + helpText, + image, + stagedImage, + stageImage, + saveImage, + resetImage, +}) => { + const [resetConfirmModalVisible, setVisible] = useState(false); + const [imageUploadKey, setKey] = useState(1); + const showDeleteModal = () => setVisible(true); + const closeDeleteModal = () => setVisible(false); + const isLogo = imageType === 'logo'; + const imageText = isLogo ? LOGO_TEXT : ICON_TEXT; + const defaultImage = isLogo ? defaultLogo : defaultIcon; + + const handleUpload = async (files: FileList | null) => { + if (!files || files.length < 1) { + return stageImage(null); + } + const file = files[0]; + const img = await readUploadedFileAsBase64(file); + stageImage(img); + }; + + const resetConfirmModal = ( + { + resetImage(); + closeDeleteModal(); + }} + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={RESET_DEFAULT_BUTTON_LABEL} + buttonColor="danger" + defaultFocusedButton="confirm" + > + <> +

{isLogo ? RESET_LOGO_DESCRIPTION : RESET_ICON_DESCRIPTION}

+

{RESET_IMAGE_CONFIRMATION_TEXT}

+ +
+ ); + + // EUI currently does not support clearing an upload input programatically, so we can render a new + // one each time the image is changed. + useEffect(() => { + setKey(imageUploadKey + 1); + }, [image]); + + return ( + <> + + {description} + + } + > + <> + + {`${BRAND_TEXT} + + + + + + + + + {SAVE_BUTTON_LABEL} + + + + {image && ( + + {RESET_DEFAULT_BUTTON_LABEL} + + )} + + + + {resetConfirmModalVisible && resetConfirmModal} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx index 15d0db4c415d..9b17ec560ba5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx @@ -17,6 +17,7 @@ import { EuiFieldText } from '@elastic/eui'; import { ContentSection } from '../../../components/shared/content_section'; +import { BrandingSection } from './branding_section'; import { Customize } from './customize'; describe('Customize', () => { @@ -32,6 +33,7 @@ describe('Customize', () => { const wrapper = shallow(); expect(wrapper.find(ContentSection)).toHaveLength(1); + expect(wrapper.find(BrandingSection)).toHaveLength(2); }); it('handles input change', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx index 98662585ce33..be4be08f54eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -9,7 +9,14 @@ import React, { FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; import { WorkplaceSearchPageTemplate } from '../../../components/layout'; import { ContentSection } from '../../../components/shared/content_section'; @@ -20,11 +27,25 @@ import { CUSTOMIZE_NAME_LABEL, CUSTOMIZE_NAME_BUTTON, } from '../../../constants'; +import { LOGO_DESCRIPTION, LOGO_HELP_TEXT, ICON_DESCRIPTION, ICON_HELP_TEXT } from '../constants'; import { SettingsLogic } from '../settings_logic'; +import { BrandingSection } from './branding_section'; + export const Customize: React.FC = () => { - const { onOrgNameInputChange, updateOrgName } = useActions(SettingsLogic); - const { orgNameInputValue } = useValues(SettingsLogic); + const { + onOrgNameInputChange, + updateOrgName, + setStagedIcon, + setStagedLogo, + updateOrgLogo, + updateOrgIcon, + resetOrgLogo, + resetOrgIcon, + } = useActions(SettingsLogic); + const { dataLoading, orgNameInputValue, icon, stagedIcon, logo, stagedLogo } = useValues( + SettingsLogic + ); const handleSubmit = (e: FormEvent) => { e.preventDefault(); @@ -38,6 +59,7 @@ export const Customize: React.FC = () => { pageTitle: CUSTOMIZE_HEADER_TITLE, description: CUSTOMIZE_HEADER_DESCRIPTION, }} + isLoading={dataLoading} >
@@ -63,6 +85,28 @@ export const Customize: React.FC = () => {
+ + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts new file mode 100644 index 000000000000..1bcd03894711 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts @@ -0,0 +1,93 @@ +/* + * 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 LOGO_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoText', + { + defaultMessage: 'logo', + } +); + +export const ICON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconText', + { + defaultMessage: 'icon', + } +); + +export const RESET_IMAGE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetImageTitle', + { + defaultMessage: 'Reset to default branding', + } +); + +export const RESET_LOGO_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetLogoDescription', + { + defaultMessage: "You're about to reset the logo to the default Workplace Search branding.", + } +); + +export const RESET_ICON_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetIconDescription', + { + defaultMessage: "You're about to reset the icon to the default Workplace Search branding.", + } +); + +export const RESET_IMAGE_CONFIRMATION_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetImageConfirmationText', + { + defaultMessage: 'Are you sure you want to do this?', + } +); + +export const ORGANIZATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.organizationLabel', + { + defaultMessage: 'Organization', + } +); + +export const BRAND_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.brandText', + { + defaultMessage: 'Brand', + } +); + +export const LOGO_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoDescription', + { + defaultMessage: 'Used as the main visual branding element across prebuilt search applications', + } +); + +export const LOGO_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoHelpText', + { + defaultMessage: 'Maximum file size is 2MB. Only PNG files are supported.', + } +); + +export const ICON_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconDescription', + { + defaultMessage: 'Used as the branding element for smaller screen sizes and browser icons', + } +); + +export const ICON_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconHelpText', + { + defaultMessage: + 'Maximum file size is 2MB and recommended aspect ratio is 1:1. Only PNG files are supported.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index 0aef84ccf20e..005f2f016d56 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -25,7 +25,7 @@ describe('SettingsLogic', () => { const { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, setQueuedSuccessMessage, } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SettingsLogic); @@ -35,8 +35,12 @@ describe('SettingsLogic', () => { connectors: [], orgNameInputValue: '', oauthApplication: null, + icon: null, + stagedIcon: null, + logo: null, + stagedLogo: null, }; - const serverProps = { organizationName: ORG_NAME, oauthApplication }; + const serverProps = { organizationName: ORG_NAME, oauthApplication, logo: null, icon: null }; beforeEach(() => { jest.clearAllMocks(); @@ -79,6 +83,34 @@ describe('SettingsLogic', () => { expect(SettingsLogic.values.oauthApplication).toEqual(oauthApplication); }); + it('setIcon', () => { + SettingsLogic.actions.setStagedIcon('stagedIcon'); + SettingsLogic.actions.setIcon('icon'); + + expect(SettingsLogic.values.icon).toEqual('icon'); + expect(SettingsLogic.values.stagedIcon).toEqual(null); + }); + + it('setStagedIcon', () => { + SettingsLogic.actions.setStagedIcon('stagedIcon'); + + expect(SettingsLogic.values.stagedIcon).toEqual('stagedIcon'); + }); + + it('setLogo', () => { + SettingsLogic.actions.setStagedLogo('stagedLogo'); + SettingsLogic.actions.setLogo('logo'); + + expect(SettingsLogic.values.logo).toEqual('logo'); + expect(SettingsLogic.values.stagedLogo).toEqual(null); + }); + + it('setStagedLogo', () => { + SettingsLogic.actions.setStagedLogo('stagedLogo'); + + expect(SettingsLogic.values.stagedLogo).toEqual('stagedLogo'); + }); + it('setUpdatedOauthApplication', () => { SettingsLogic.actions.setUpdatedOauthApplication({ oauthApplication }); @@ -143,7 +175,7 @@ describe('SettingsLogic', () => { body: JSON.stringify({ name: NAME }), }); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); }); @@ -156,6 +188,80 @@ describe('SettingsLogic', () => { }); }); + describe('updateOrgIcon', () => { + it('calls API and sets values', async () => { + const ICON = 'icon'; + SettingsLogic.actions.setStagedIcon(ICON); + const setIconSpy = jest.spyOn(SettingsLogic.actions, 'setIcon'); + http.put.mockReturnValue(Promise.resolve({ icon: ICON })); + + SettingsLogic.actions.updateOrgIcon(); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/upload_images', { + body: JSON.stringify({ icon: ICON }), + }); + await nextTick(); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(setIconSpy).toHaveBeenCalledWith(ICON); + }); + + it('handles error', async () => { + http.put.mockReturnValue(Promise.reject('this is an error')); + SettingsLogic.actions.updateOrgIcon(); + + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('updateOrgLogo', () => { + it('calls API and sets values', async () => { + const LOGO = 'logo'; + SettingsLogic.actions.setStagedLogo(LOGO); + const setLogoSpy = jest.spyOn(SettingsLogic.actions, 'setLogo'); + http.put.mockReturnValue(Promise.resolve({ logo: LOGO })); + + SettingsLogic.actions.updateOrgLogo(); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/upload_images', { + body: JSON.stringify({ logo: LOGO }), + }); + await nextTick(); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(setLogoSpy).toHaveBeenCalledWith(LOGO); + }); + + it('handles error', async () => { + http.put.mockReturnValue(Promise.reject('this is an error')); + SettingsLogic.actions.updateOrgLogo(); + + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + it('resetOrgLogo', () => { + const updateOrgLogoSpy = jest.spyOn(SettingsLogic.actions, 'updateOrgLogo'); + SettingsLogic.actions.setStagedLogo('stagedLogo'); + SettingsLogic.actions.setLogo('logo'); + SettingsLogic.actions.resetOrgLogo(); + + expect(SettingsLogic.values.logo).toEqual(null); + expect(SettingsLogic.values.stagedLogo).toEqual(null); + expect(updateOrgLogoSpy).toHaveBeenCalled(); + }); + + it('resetOrgIcon', () => { + const updateOrgIconSpy = jest.spyOn(SettingsLogic.actions, 'updateOrgIcon'); + SettingsLogic.actions.setStagedIcon('stagedIcon'); + SettingsLogic.actions.setIcon('icon'); + SettingsLogic.actions.resetOrgIcon(); + + expect(SettingsLogic.values.icon).toEqual(null); + expect(SettingsLogic.values.stagedIcon).toEqual(null); + expect(updateOrgIconSpy).toHaveBeenCalled(); + }); + describe('updateOauthApplication', () => { it('calls API and sets values', async () => { const { name, redirectUri, confidential } = oauthApplication; @@ -179,7 +285,7 @@ describe('SettingsLogic', () => { ); await nextTick(); expect(setUpdatedOauthApplicationSpy).toHaveBeenCalledWith({ oauthApplication }); - expect(setSuccessMessage).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); }); it('handles error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index e07adbde1593..65a2cdf8c3f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { clearFlashMessages, setQueuedSuccessMessage, - setSuccessMessage, + flashSuccessToast, flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -34,6 +34,8 @@ interface IOauthApplication { export interface SettingsServerProps { organizationName: string; oauthApplication: IOauthApplication; + logo: string | null; + icon: string | null; } interface SettingsActions { @@ -41,6 +43,10 @@ interface SettingsActions { onOrgNameInputChange(orgNameInputValue: string): string; setUpdatedName({ organizationName }: { organizationName: string }): string; setServerProps(props: SettingsServerProps): SettingsServerProps; + setIcon(icon: string | null): string | null; + setStagedIcon(stagedIcon: string | null): string | null; + setLogo(logo: string | null): string | null; + setStagedLogo(stagedLogo: string | null): string | null; setOauthApplication(oauthApplication: IOauthApplication): IOauthApplication; setUpdatedOauthApplication({ oauthApplication, @@ -52,6 +58,10 @@ interface SettingsActions { initializeConnectors(): void; updateOauthApplication(): void; updateOrgName(): void; + updateOrgLogo(): void; + updateOrgIcon(): void; + resetOrgLogo(): void; + resetOrgIcon(): void; deleteSourceConfig( serviceType: string, name: string @@ -66,14 +76,24 @@ interface SettingsValues { connectors: Connector[]; orgNameInputValue: string; oauthApplication: IOauthApplication | null; + logo: string | null; + icon: string | null; + stagedLogo: string | null; + stagedIcon: string | null; } +const imageRoute = '/api/workplace_search/org/settings/upload_images'; + export const SettingsLogic = kea>({ actions: { onInitializeConnectors: (connectors: Connector[]) => connectors, onOrgNameInputChange: (orgNameInputValue: string) => orgNameInputValue, setUpdatedName: ({ organizationName }) => organizationName, setServerProps: (props: SettingsServerProps) => props, + setIcon: (icon) => icon, + setStagedIcon: (stagedIcon) => stagedIcon, + setLogo: (logo) => logo, + setStagedLogo: (stagedLogo) => stagedLogo, setOauthApplication: (oauthApplication: IOauthApplication) => oauthApplication, setUpdatedOauthApplication: ({ oauthApplication }: { oauthApplication: IOauthApplication }) => oauthApplication, @@ -81,6 +101,10 @@ export const SettingsLogic = kea> initializeSettings: () => true, initializeConnectors: () => true, updateOrgName: () => true, + updateOrgLogo: () => true, + updateOrgIcon: () => true, + resetOrgLogo: () => true, + resetOrgIcon: () => true, updateOauthApplication: () => true, deleteSourceConfig: (serviceType: string, name: string) => ({ serviceType, @@ -113,10 +137,43 @@ export const SettingsLogic = kea> dataLoading: [ true, { + setServerProps: () => false, onInitializeConnectors: () => false, resetSettingsState: () => true, }, ], + logo: [ + null, + { + setServerProps: (_, { logo }) => logo, + setLogo: (_, logo) => logo, + resetOrgLogo: () => null, + }, + ], + stagedLogo: [ + null, + { + setStagedLogo: (_, stagedLogo) => stagedLogo, + resetOrgLogo: () => null, + setLogo: () => null, + }, + ], + icon: [ + null, + { + setServerProps: (_, { icon }) => icon, + setIcon: (_, icon) => icon, + resetOrgIcon: () => null, + }, + ], + stagedIcon: [ + null, + { + setStagedIcon: (_, stagedIcon) => stagedIcon, + resetOrgIcon: () => null, + setIcon: () => null, + }, + ], }, listeners: ({ actions, values }) => ({ initializeSettings: async () => { @@ -150,12 +207,38 @@ export const SettingsLogic = kea> try { const response = await http.put(route, { body }); actions.setUpdatedName(response); - setSuccessMessage(ORG_UPDATED_MESSAGE); + flashSuccessToast(ORG_UPDATED_MESSAGE); AppLogic.actions.setOrgName(name); } catch (e) { flashAPIErrors(e); } }, + updateOrgLogo: async () => { + const { http } = HttpLogic.values; + const { stagedLogo: logo } = values; + const body = JSON.stringify({ logo }); + + try { + const response = await http.put(imageRoute, { body }); + actions.setLogo(response.logo); + flashSuccessToast(ORG_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + updateOrgIcon: async () => { + const { http } = HttpLogic.values; + const { stagedIcon: icon } = values; + const body = JSON.stringify({ icon }); + + try { + const response = await http.put(imageRoute, { body }); + actions.setIcon(response.icon); + flashSuccessToast(ORG_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, updateOauthApplication: async () => { const { http } = HttpLogic.values; const route = '/api/workplace_search/org/settings/oauth_application'; @@ -170,7 +253,7 @@ export const SettingsLogic = kea> try { const response = await http.put(route, { body }); actions.setUpdatedOauthApplication(response); - setSuccessMessage(OAUTH_APP_UPDATED_MESSAGE); + flashSuccessToast(OAUTH_APP_UPDATED_MESSAGE); } catch (e) { flashAPIErrors(e); } @@ -195,5 +278,11 @@ export const SettingsLogic = kea> resetSettingsState: () => { clearFlashMessages(); }, + resetOrgLogo: () => { + actions.updateOrgLogo(); + }, + resetOrgIcon: () => { + actions.updateOrgIcon(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 3107afbf46cd..b0e286077f83 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -365,4 +365,133 @@ describe('crawler routes', () => { mockRouter.shouldThrow(request); }); }); + + describe('GET /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + body: { unit: 'day', frequency: 7 }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + body: { unit: 'day', frequency: 7 }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a unit property in body', () => { + const request = { + params: { name: 'some-engine' }, + body: { frequency: 7 }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a frequency property in body', () => { + const request = { + params: { name: 'some-engine' }, + body: { unit: 'day' }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + }; + mockRouter.shouldThrow(request); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 6374b7c83de8..1ba7885664ff 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -158,4 +158,50 @@ export function registerCrawlerRoutes({ path: '/api/as/v0/engines/:name/crawler/process_crawls', }) ); + + router.get( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); + + router.put( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + unit: schema.string(), + frequency: schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts index 00a5b6c75df0..858bd71c50c4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts @@ -10,6 +10,7 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks_ import { registerOrgSettingsRoute, registerOrgSettingsCustomizeRoute, + registerOrgSettingsUploadImagesRoute, registerOrgSettingsOauthApplicationRoute, } from './settings'; @@ -67,6 +68,36 @@ describe('settings routes', () => { }); }); + describe('PUT /api/workplace_search/org/settings/upload_images', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/settings/upload_images', + }); + + registerOrgSettingsUploadImagesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/settings/upload_images', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: { logo: 'foo', icon: null } }; + mockRouter.shouldValidate(request); + }); + }); + }); + describe('PUT /api/workplace_search/org/settings/oauth_application', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts index bd8b5388625c..aa8651f74bec 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts @@ -43,6 +43,26 @@ export function registerOrgSettingsCustomizeRoute({ ); } +export function registerOrgSettingsUploadImagesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/settings/upload_images', + validate: { + body: schema.object({ + logo: schema.maybe(schema.nullable(schema.string())), + icon: schema.maybe(schema.nullable(schema.string())), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/settings/upload_images', + }) + ); +} + export function registerOrgSettingsOauthApplicationRoute({ router, enterpriseSearchRequestHandler, @@ -69,5 +89,6 @@ export function registerOrgSettingsOauthApplicationRoute({ export const registerSettingsRoutes = (dependencies: RouteDependencies) => { registerOrgSettingsRoute(dependencies); registerOrgSettingsCustomizeRoute(dependencies); + registerOrgSettingsUploadImagesRoute(dependencies); registerOrgSettingsOauthApplicationRoute(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 976cdfadca4b..481c4527d597 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/event_log/tsconfig.json b/x-pack/plugins/event_log/tsconfig.json index 9b7cde10da3d..e0e72fdbf658 100644 --- a/x-pack/plugins/event_log/tsconfig.json +++ b/x-pack/plugins/event_log/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json index 1260af55fbff..b16d7b47bba5 100644 --- a/x-pack/plugins/features/tsconfig.json +++ b/x-pack/plugins/features/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index e10b9e90a71d..8462f8983a67 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -111,6 +111,10 @@ export interface ImportResponse { export interface ImportFailure { item: number; reason: string; + caused_by?: { + type: string; + reason: string; + }; doc: ImportDoc; } diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index f93d73647ed0..deb170974ced 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -164,6 +164,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { failures.push({ item: i, reason: item.index.error.reason, + caused_by: item.index.error.caused_by, doc: data[i], }); } diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 3e146d76fbb9..efea61e38b3e 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 1e51f2d6163c..5294c31d6a28 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -81,6 +81,10 @@ export const packagePolicyRouteService = { getDeletePath: () => { return PACKAGE_POLICY_API_ROUTES.DELETE_PATTERN; }, + + getUpgradePath: () => { + return PACKAGE_POLICY_API_ROUTES.UPGRADE_PATTERN; + }, }; export const agentPolicyRouteService = { diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 59691bf32d09..0deda3bf3265 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -7,6 +7,7 @@ export * from './models'; export * from './rest_spec'; + import type { PreconfiguredAgentPolicy, PreconfiguredPackage } from './models/preconfiguration'; export interface FleetConfigType { diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index 2ff21961f154..ab977c5d67d0 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -81,4 +81,5 @@ export type PackagePolicySOAttributes = Omit; export type DryRunPackagePolicy = NewPackagePolicy & { errors?: Array<{ key: string | undefined; message: string }>; + missingVars?: string[]; }; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index e9e4d40f25f6..ed5f8e07098d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -10,6 +10,7 @@ import type { NewPackagePolicy, UpdatePackagePolicy, DryRunPackagePolicy, + PackagePolicyPackage, } from '../models'; export interface GetPackagePoliciesRequest { @@ -61,6 +62,7 @@ export type DeletePackagePoliciesResponse = Array<{ id: string; name?: string; success: boolean; + package?: PackagePolicyPackage; }>; export interface UpgradePackagePolicyBaseResponse { diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index bbf60ef82ee6..f9ad1b0b966a 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -1,5 +1,9 @@ { "id": "fleet", + "owner": { + "name": "Fleet", + "githubTeam": "fleet" + }, "version": "kibana", "server": true, "ui": true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index dd24f1091843..59498325bf91 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -21,10 +21,10 @@ import { import { WithHeaderLayout } from '../../../../layouts'; import type { AgentPolicy, PackageInfo, RegistryPolicyTemplate } from '../../../../types'; import { PackageIcon } from '../../../../components'; -import type { CreatePackagePolicyFrom } from '../types'; +import type { EditPackagePolicyFrom } from '../types'; export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ - from: CreatePackagePolicyFrom; + from: EditPackagePolicyFrom; cancelUrl: string; onCancel?: React.ReactEventHandler; agentPolicy?: AgentPolicy; @@ -48,11 +48,48 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ 'data-test-subj': dataTestSubj, tabs = [], }) => { + const isAdd = useMemo(() => ['package'].includes(from), [from]); + const isEdit = useMemo(() => ['edit', 'package-edit'].includes(from), [from]); + const isUpgrade = useMemo( + () => + ['upgrade-from-fleet-policy-list', 'upgrade-from-integrations-policy-list'].includes(from), + [from] + ); + const pageTitle = useMemo(() => { - if ( - (from === 'package' || from === 'package-edit' || from === 'edit' || from === 'policy') && - packageInfo - ) { + if ((isAdd || isEdit || isUpgrade) && packageInfo) { + let pageTitleText = ( + + ); + + if (isEdit) { + pageTitleText = ( + + ); + } else if (isUpgrade) { + pageTitleText = ( + + ); + } + return ( @@ -66,32 +103,14 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ -

- {from === 'edit' || from === 'package-edit' ? ( - - ) : ( - - )} -

+

{pageTitleText}

); } - return from === 'edit' || from === 'package-edit' ? ( + return isEdit ? (

{ - return from === 'edit' || from === 'package-edit' ? ( - - ) : from === 'policy' ? ( - - ) : ( - - ); - }, [from]); + if (isEdit) { + return ( + + ); + } else if (isAdd) { + return ( + + ); + } else if (isUpgrade) { + return ( + + ); + } else { + return ( + + ); + } + }, [isAdd, isEdit, isUpgrade]); const leftColumn = ( @@ -167,7 +201,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ); const rightColumn = - agentPolicy && (from === 'policy' || from === 'edit') ? ( + agentPolicy && (isAdd || isEdit) ? ( { * We may want to deprecate the ability to pass in policyId from URL params since there is no package * creation possible if a user has not chosen one from the packages UI. */ - const from: CreatePackagePolicyFrom = + const from: EditPackagePolicyFrom = 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package'; // Agent policy and package info states diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index a4d7b5a952b6..ba48553edad9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -5,5 +5,11 @@ * 2.0. */ -export type CreatePackagePolicyFrom = 'package' | 'package-edit' | 'policy' | 'edit'; +export type EditPackagePolicyFrom = + | 'package' + | 'package-edit' + | 'policy' + | 'edit' + | 'upgrade-from-fleet-policy-list' + | 'upgrade-from-integrations-policy-list'; export type PackagePolicyFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index 0d2d8e188218..4f5c61b76f4e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -16,19 +16,20 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common'; import { pagePathGetters } from '../../../../../../../constants'; -import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../types'; import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components'; -import { useCapabilities, useStartServices } from '../../../../../hooks'; - -interface InMemoryPackagePolicy extends PackagePolicy { - packageName?: string; - packageTitle?: string; - packageVersion?: string; -} +import { + useCapabilities, + useLink, + usePackageInstallations, + useStartServices, +} from '../../../../../hooks'; interface Props { packagePolicies: PackagePolicy[]; @@ -53,6 +54,8 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }) => { const { application } = useStartServices(); const hasWriteCapabilities = useCapabilities().write; + const { updatableIntegrations } = usePackageInstallations(); + const { getHref } = useLink(); // With the package policies provided on input, generate the list of package policies // used in the InMemoryTable (flattens some values for search) as well as @@ -66,11 +69,22 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.push(packagePolicy.namespace); } + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + return { ...packagePolicy, packageName: packagePolicy.package?.name ?? '', packageTitle: packagePolicy.package?.title ?? '', packageVersion: packagePolicy.package?.version ?? '', + hasUpgrade, }; } ); @@ -79,7 +93,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ inputTypesValues.sort(stringSortAscending); return [mappedPackagePolicies, namespacesValues.map(toFilterOption)]; - }, [originalPackagePolicies]); + }, [originalPackagePolicies, updatableIntegrations]); const columns = useMemo( (): EuiInMemoryTableProps['columns'] => [ @@ -89,24 +103,20 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), - render: (value: string) => ( - - {value} - - ), - }, - { - field: 'description', - name: i18n.translate( - 'xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle', - { - defaultMessage: 'Description', - } - ), - render: (value: string) => ( - - {value} - + render: (value: string, { description }) => ( + <> + + {value} + + {description ? ( + +   + + + + + ) : null} + ), }, { @@ -143,6 +153,35 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ )} + {packagePolicy.hasUpgrade && ( + <> + + + + + + + + + + + + )} ); }, @@ -167,14 +206,21 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { render: (packagePolicy: InMemoryPackagePolicy) => { return ( - + ); }, }, ], }, ], - [agentPolicy] + [agentPolicy, getHref] ); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 3dc88c7565e7..8a1ffe21a90e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -6,19 +6,28 @@ */ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; -import { useRouteMatch, useHistory } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiButton, EuiBottomBar, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiLink, + EuiFlyout, + EuiCodeBlock, + EuiPortal, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, } from '@elastic/eui'; +import styled from 'styled-components'; -import type { AgentPolicy, PackageInfo, UpdatePackagePolicy } from '../../../types'; +import type { AgentPolicy, PackageInfo, UpdatePackagePolicy, PackagePolicy } from '../../../types'; import { useLink, useBreadcrumbs, @@ -30,6 +39,8 @@ import { sendGetOneAgentPolicy, sendGetOnePackagePolicy, sendGetPackageInfoByKey, + sendUpgradePackagePolicy, + sendUpgradePackagePolicyDryRun, } from '../../../hooks'; import { useBreadcrumbs as useIntegrationsBreadcrumbs } from '../../../../integrations/hooks'; import { Loading, Error, ExtensionWrapper } from '../../../components'; @@ -39,13 +50,16 @@ import type { PackagePolicyValidationResults } from '../create_package_policy_pa import { validatePackagePolicy, validationHasErrors } from '../create_package_policy_page/services'; import type { PackagePolicyFormState, - CreatePackagePolicyFrom, + EditPackagePolicyFrom, } from '../create_package_policy_page/types'; import { StepConfigurePackagePolicy } from '../create_package_policy_page/step_configure_package'; import { StepDefinePackagePolicy } from '../create_package_policy_page/step_define_package_policy'; -import type { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; +import type { + GetOnePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, +} from '../../../../../../common/types/rest_spec'; import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; -import { pkgKeyFromPackageInfo } from '../../../services'; +import { pkgKeyFromPackageInfo, storedPackagePoliciesToAgentInputs } from '../../../services'; export const EditPackagePolicyPage = memo(() => { const { @@ -57,14 +71,13 @@ export const EditPackagePolicyPage = memo(() => { export const EditPackagePolicyForm = memo<{ packagePolicyId: string; - from?: CreatePackagePolicyFrom; + from?: EditPackagePolicyFrom; }>(({ packagePolicyId, from = 'edit' }) => { - const { notifications } = useStartServices(); + const { application, notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const history = useHistory(); - const { getHref, getPath } = useLink(); + const { getHref } = useLink(); // Agent policy, package info, and package policy states const [isLoadingData, setIsLoadingData] = useState(true); @@ -84,6 +97,10 @@ export const EditPackagePolicyForm = memo<{ const [originalPackagePolicy, setOriginalPackagePolicy] = useState< GetOnePackagePolicyResponse['item'] >(); + const [dryRunData, setDryRunData] = useState(); + + const isUpgrade = + from === 'upgrade-from-fleet-policy-list' || from === 'upgrade-from-integrations-policy-list'; const policyId = agentPolicy?.id ?? ''; @@ -113,8 +130,30 @@ export const EditPackagePolicyForm = memo<{ if (agentPolicyData?.item) { setAgentPolicy(agentPolicyData.item); } - if (packagePolicyData?.item) { - setOriginalPackagePolicy(packagePolicyData.item); + + const { data: upgradePackagePolicyDryRunData } = await sendUpgradePackagePolicyDryRun([ + packagePolicyId, + ]); + + if (upgradePackagePolicyDryRunData) { + setDryRunData(upgradePackagePolicyDryRunData); + } + + const basePolicy: PackagePolicy | undefined = packagePolicyData?.item; + let baseInputs: any = basePolicy?.inputs; + let basePackage: any = basePolicy?.package; + + const proposedUpgradePackagePolicy = upgradePackagePolicyDryRunData?.[0]?.diff?.[1]; + + // If we're upgrading the package, we need to "start from" the policy as it's returned from + // the dry run so we can allow the user to edit any new variables before saving + upgrading + if (isUpgrade && !!proposedUpgradePackagePolicy) { + baseInputs = proposedUpgradePackagePolicy.inputs; + basePackage = proposedUpgradePackagePolicy.package; + } + + if (basePolicy) { + setOriginalPackagePolicy(basePolicy); const { id, @@ -127,28 +166,41 @@ export const EditPackagePolicyForm = memo<{ updated_at, /* eslint-enable @typescript-eslint/naming-convention */ ...restOfPackagePolicy - } = packagePolicyData.item; + } = basePolicy as any; // Remove `compiled_stream` from all stream info, we assign this after saving const newPackagePolicy = { ...restOfPackagePolicy, - inputs: inputs.map((input) => { + inputs: baseInputs.map((input: any) => { // Remove `compiled_input` from all input info, we assign this after saving const { streams, compiled_input: compiledInput, ...restOfInput } = input; return { ...restOfInput, - streams: streams.map((stream) => { + streams: streams.map((stream: any) => { // eslint-disable-next-line @typescript-eslint/naming-convention const { compiled_stream, ...restOfStream } = stream; return restOfStream; }), }; }), + package: basePackage, }; + setPackagePolicy(newPackagePolicy); - if (packagePolicyData.item.package) { + + if (basePolicy.package) { + let _packageInfo = basePolicy.package; + + // When upgrading, we need to grab the `packageInfo` data from the new package version's + // proposed policy (comes from the dry run diff) to ensure we have the valid package key/version + // before saving + if (isUpgrade && !!upgradePackagePolicyDryRunData?.[0]?.diff?.[1]?.package) { + _packageInfo = upgradePackagePolicyDryRunData[0].diff?.[1]?.package; + } + const { data: packageData } = await sendGetPackageInfoByKey( - pkgKeyFromPackageInfo(packagePolicyData.item.package) + pkgKeyFromPackageInfo(_packageInfo!) ); + if (packageData?.response) { setPackageInfo(packageData.response); setValidationResults(validatePackagePolicy(newPackagePolicy, packageData.response)); @@ -162,7 +214,7 @@ export const EditPackagePolicyForm = memo<{ setIsLoadingData(false); }; getData(); - }, [policyId, packagePolicyId]); + }, [policyId, packagePolicyId, isUpgrade]); // Retrieve agent count const [agentCount, setAgentCount] = useState(0); @@ -240,7 +292,7 @@ export const EditPackagePolicyForm = memo<{ // Cancel url + Success redirect Path: // if `from === 'edit'` then it links back to Policy Details - // if `from === 'package-edit'` then it links back to the Integration Policy List + // if `from === 'package-edit'`, or `upgrade-from-integrations-policy-list` then it links back to the Integration Policy List const cancelUrl = useMemo((): string => { if (packageInfo && policyId) { return from === 'package-edit' @@ -254,14 +306,14 @@ export const EditPackagePolicyForm = memo<{ const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { - return from === 'package-edit' - ? getPath('integration_details_policies', { + return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list' + ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), }) - : getPath('policy_details', { policyId }); + : getHref('policy_details', { policyId }); } return '/'; - }, [from, getPath, packageInfo, policyId]); + }, [from, getHref, packageInfo, policyId]); // Save package policy const [formState, setFormState] = useState('INVALID'); @@ -281,9 +333,33 @@ export const EditPackagePolicyForm = memo<{ setFormState('CONFIRM'); return; } + const { error } = await savePackagePolicy(); if (!error) { - history.push(successRedirectPath); + if (isUpgrade) { + const { error: upgradeError } = await sendUpgradePackagePolicy([packagePolicyId]); + + if (upgradeError) { + notifications.toasts.addError(upgradeError, { + title: i18n.translate('xpack.fleet.upgradePackagePolicy.failedNotificationTitle', { + defaultMessage: 'Error upgrading {packagePolicyName}', + values: { + packagePolicyName: packagePolicy.name, + }, + }), + toastMessage: i18n.translate( + 'xpack.fleet.editPackagePolicy.failedConflictNotificationMessage', + { + defaultMessage: `Data is out of date. Refresh the page to get the latest policy.`, + } + ), + }); + + return; + } + } + + application.navigateToUrl(successRedirectPath); notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', { defaultMessage: `Successfully updated '{packagePolicyName}'`, @@ -468,10 +544,20 @@ export const EditPackagePolicyForm = memo<{ onCancel={() => setFormState('VALID')} /> )} + + {isUpgrade && dryRunData && ( + <> + + + + )} + {configurePackage} + {/* Extra space to accomodate the EuiBottomBar height */} + @@ -526,6 +612,101 @@ const PoliciesBreadcrumb: React.FunctionComponent<{ policyName: string; policyId return null; }; +const UpgradeStatusCallout: React.FunctionComponent<{ + dryRunData: UpgradePackagePolicyDryRunResponse; +}> = ({ dryRunData }) => { + const [isPreviousVersionFlyoutOpen, setIsPreviousVersionFlyoutOpen] = useState(false); + + if (!dryRunData) { + return null; + } + + const isReadyForUpgrade = !dryRunData[0].hasErrors; + + const [currentPackagePolicy, proposedUpgradePackagePolicy] = dryRunData[0].diff || []; + + const FlyoutBody = styled(EuiFlyoutBody)` + .euiFlyoutBody__overflowContent { + padding: 0; + } + `; + + return ( + <> + {isPreviousVersionFlyoutOpen && currentPackagePolicy && ( + + setIsPreviousVersionFlyoutOpen(false)} size="l" maxWidth={640}> + + +

+ +

+
+
+ + + {JSON.stringify( + storedPackagePoliciesToAgentInputs([currentPackagePolicy]), + null, + 2 + )} + + +
+
+ )} + + {isReadyForUpgrade ? ( + + + + ) : ( + + setIsPreviousVersionFlyoutOpen(true)}> + + + ), + }} + /> + + )} + + ); +}; + const IntegrationsBreadcrumb = memo<{ pkgTitle: string; policyName: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx index 19f0216a39e0..9d9077a9abdb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx @@ -17,6 +17,7 @@ import { AgentPolicyListPage } from './list_page'; import { AgentPolicyDetailsPage } from './details_page'; import { CreatePackagePolicyPage } from './create_package_policy_page'; import { EditPackagePolicyPage } from './edit_package_policy_page'; +import { UpgradePackagePolicyPage } from './upgrade_package_policy_page'; export const AgentPolicyApp: React.FunctionComponent = () => { useBreadcrumbs('policies'); @@ -28,6 +29,9 @@ export const AgentPolicyApp: React.FunctionComponent = () => { + + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx new file mode 100644 index 000000000000..e9442f84d228 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; + +import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; + +import { EditPackagePolicyForm } from '../edit_package_policy_page'; + +export const UpgradePackagePolicyPage = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + const { search } = useLocation(); + + const qs = new URLSearchParams(search); + const fromQs = qs.get('from'); + + let from: EditPackagePolicyFrom | undefined; + + // Shorten query strings to make them more presentable in the URL + if (fromQs && fromQs === 'fleet-policy-list') { + from = 'upgrade-from-fleet-policy-list'; + } else if (fromQs && fromQs === 'integrations-policy-list') { + from = 'upgrade-from-integrations-policy-list'; + } + + return ; +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index b7f044040e43..bdcfb1ebb653 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -28,12 +28,14 @@ import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; +import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; import { useLink, useUrlPagination, useGetPackageInstallStatus, AgentPolicyRefreshContext, useUIExtension, + usePackageInstallations, } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { @@ -92,9 +94,39 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps perPage: pagination.pageSize, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, }); - + const { updatableIntegrations } = usePackageInstallations(); const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout'); + const packageAndAgentPolicies = useMemo((): Array<{ + agentPolicy: GetAgentPoliciesResponseItem; + packagePolicy: InMemoryPackagePolicy; + }> => { + if (!data?.items) { + return []; + } + + const newPolicies = data.items.map(({ agentPolicy, packagePolicy }) => { + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + return { + agentPolicy, + packagePolicy: { + ...packagePolicy, + hasUpgrade, + }, + }; + }); + + return newPolicies; + }, [data?.items, updatableIntegrations]); + // Handle the "add agent" link displayed in post-installation toast notifications in the case // where a user is clicking the link while on the package policies listing page useEffect(() => { @@ -180,6 +212,49 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { defaultMessage: 'Version', }), + render(_version, { agentPolicy, packagePolicy }) { + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + + return ( + + + + + + + + {hasUpgrade && ( + + + + + + )} + + ); + }, }, { field: 'packagePolicy.policy_id', @@ -274,12 +349,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps packagePolicy={packagePolicy} viewDataStep={viewDataStep} showAddAgent={true} + upgradePackagePolicyHref={`${getHref('upgrade_package_policy', { + policyId: agentPolicy.id, + packagePolicyId: packagePolicy.id, + })}?from=integrations-policy-list`} /> ); }, }, ], - [viewDataStep] + [getHref, updatableIntegrations, viewDataStep] ); const noItemsMessage = useMemo(() => { @@ -314,7 +393,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps agentPolicy.id === flyoutOpenForPolicyId) - ?.agentPolicy + packageAndAgentPolicies.find( + ({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId + )?.agentPolicy } viewDataStep={viewDataStep} /> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 74572e74dd50..b260bba493e7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -141,6 +141,9 @@ const AvailablePackages: React.FC = memo(() => { const queryParams = new URLSearchParams(useLocation().search); const initialCategory = queryParams.get('category') || ''; const [selectedCategory, setSelectedCategory] = useState(initialCategory); + const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({ + category: '', + }); const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ category: selectedCategory, }); @@ -152,6 +155,11 @@ const AvailablePackages: React.FC = memo(() => { [categoryPackagesRes] ); + const allPackages = useMemo( + () => packageListToIntegrationsList(allCategoryPackagesRes?.response || []), + [allCategoryPackagesRes] + ); + const title = useMemo( () => i18n.translate('xpack.fleet.epmList.allTitle', { @@ -167,16 +175,16 @@ const AvailablePackages: React.FC = memo(() => { title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', { defaultMessage: 'All', }), - count: packages?.length || 0, + count: allPackages?.length || 0, }, ...(categoriesRes ? categoriesRes.response : []), ], - [packages?.length, categoriesRes] + [allPackages?.length, categoriesRes] ); const controls = categories ? ( { diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 01e2df6cc410..a87cd7e39bfb 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -10,7 +10,7 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy, PackagePolicy } from '../types'; +import type { AgentPolicy, InMemoryPackagePolicy } from '../types'; import { useAgentPolicyRefresh, useCapabilities, useLink } from '../hooks'; @@ -21,10 +21,11 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ agentPolicy: AgentPolicy; - packagePolicy: PackagePolicy; + packagePolicy: InMemoryPackagePolicy; viewDataStep?: EuiStepProps; showAddAgent?: boolean; -}> = ({ agentPolicy, packagePolicy, viewDataStep, showAddAgent }) => { + upgradePackagePolicyHref: string; +}> = ({ agentPolicy, packagePolicy, viewDataStep, showAddAgent, upgradePackagePolicyHref }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -79,6 +80,16 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ defaultMessage="Edit integration" /> , + + + , // FIXME: implement Copy package policy action // {}} key="packagePolicyCopy"> // [ + FLEET_BASE_PATH, + `/policies/${policyId}/upgrade-package-policy/${packagePolicyId}`, + ], agent_list: ({ kuery }) => [FLEET_BASE_PATH, `/agents${kuery ? `?kuery=${kuery}` : ''}`], agent_details: ({ agentId, tabId, logQuery }) => [ FLEET_BASE_PATH, diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index a00c0c5dacf1..c41dd1ad42a7 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -26,3 +26,4 @@ export * from './use_ui_extension'; export * from './use_intra_app_state'; export * from './use_platform'; export * from './use_agent_policy_refresh'; +export * from './use_package_installations'; diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index f8202c71f600..f4735e6f8554 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -14,6 +14,18 @@ import type { PackagePolicy } from '../types'; import { useGetPackages } from './use_request/epm'; import { useGetAgentPolicies } from './use_request/agent_policy'; +interface UpdatableIntegration { + currentVersion: string; + policiesToUpgrade: Array<{ + id: string; + name: string; + agentsCount: number; + pkgPolicyId: string; + pkgPolicyName: string; + pkgPolicyIntegrationVersion: string; + }>; +} + export const usePackageInstallations = () => { const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ experimental: true, @@ -38,7 +50,7 @@ export const usePackageInstallations = () => { [allInstalledPackages] ); - const updatableIntegrations = useMemo( + const updatableIntegrations = useMemo>( () => (agentPolicyData?.items || []).reduce((result, policy) => { policy.package_policies.forEach((pkgPolicy: PackagePolicy | string) => { @@ -60,7 +72,7 @@ export const usePackageInstallations = () => { packageData.policiesToUpgrade.push({ id: policy.id, name: policy.name, - agentsCount: policy.agents, + agentsCount: policy.agents ?? 0, pkgPolicyId: pkgPolicy.id, pkgPolicyName: pkgPolicy.name, pkgPolicyIntegrationVersion: version, @@ -69,7 +81,7 @@ export const usePackageInstallations = () => { } }); return result; - }, new Map()), + }, new Map()), [allInstalledPackages, agentPolicyData] ); diff --git a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts index 5057b6f51b2c..d39b15a3b3bf 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts @@ -18,6 +18,8 @@ import type { GetPackagePoliciesRequest, GetPackagePoliciesResponse, GetOnePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, + UpgradePackagePolicyResponse, } from '../../../common/types/rest_spec'; import { sendRequest, useRequest } from './use_request'; @@ -63,3 +65,25 @@ export const sendGetOnePackagePolicy = (packagePolicyId: string) => { method: 'get', }); }; + +export function sendUpgradePackagePolicyDryRun(packagePolicyIds: string[]) { + return sendRequest({ + path: packagePolicyRouteService.getUpgradePath(), + method: 'post', + body: JSON.stringify({ + packagePolicyIds, + dryRun: true, + }), + }); +} + +export function sendUpgradePackagePolicy(packagePolicyIds: string[]) { + return sendRequest({ + path: packagePolicyRouteService.getUpgradePath(), + method: 'post', + body: JSON.stringify({ + packagePolicyIds, + dryRun: false, + }), + }); +} diff --git a/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts b/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts new file mode 100644 index 000000000000..afdcc9239608 --- /dev/null +++ b/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackagePolicy } from '../../common/types'; + +// Used in list view tables where virtual/flattened fields are added +export interface InMemoryPackagePolicy extends PackagePolicy { + packageName?: string; + packageTitle?: string; + packageVersion?: string; + hasUpgrade: boolean; +} diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index c91ec42d3e52..2328ca826da7 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -50,6 +50,9 @@ export { UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, GetPackagePoliciesResponse, + DryRunPackagePolicy, + UpgradePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents @@ -128,5 +131,5 @@ export { } from '../../common'; export * from './intra_app_route_state'; - export * from './ui_extensions'; +export * from './in_memory_package_policy'; diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 6a9a4cd9ba83..716c81573e85 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -44,7 +44,7 @@ export const getAgentUsage = async ( error, offline, updating, - } = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient); + } = await AgentService.getAgentStatusForAgentPolicy(esClient); return { total_enrolled: total, healthy: online, diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index 9616ba11545e..47440e791747 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -76,7 +76,6 @@ export const getFleetServerUsage = async ( } const { total, inactive, online, error, updating, offline } = await getAgentStatusForAgentPolicy( - soClient, esClient, undefined, Array.from(policyIds) diff --git a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index eb70aff3238a..5f9a4bfde633 100644 --- a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -36,7 +36,10 @@ export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = { }, }; +export const FLEET_FINAL_PIPELINE_VERSION = 1; +// If the content is updated you probably need to update the FLEET_FINAL_PIPELINE_VERSION too to allow upgrade of the pipeline export const FLEET_FINAL_PIPELINE_CONTENT = `--- +version: ${FLEET_FINAL_PIPELINE_VERSION} description: > Final pipeline for processing all incoming Fleet Agent documents. processors: diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 3aca5e8800dc..28f3ea96f732 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -63,4 +63,5 @@ export { FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, FLEET_FINAL_PIPELINE_ID, FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_VERSION, } from './fleet_es_assets'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index ab1cd9002d04..8841c897fcb2 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -23,7 +23,14 @@ export { ArtifactsClientInterface, Artifact, } from './services'; -export { FleetSetupContract, FleetSetupDeps, FleetStartContract, ExternalCallback } from './plugin'; + +export { FleetSetupContract, FleetSetupDeps, FleetStartContract } from './plugin'; +export type { + ExternalCallback, + PutPackagePolicyUpdateCallback, + PostPackagePolicyDeleteCallback, + PostPackagePolicyCreateCallback, +} from './types'; export { AgentNotFoundError } from './errors'; export const config: PluginConfigDescriptor = { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index c4ba7e363bc5..9f07dfac9670 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -101,6 +101,7 @@ export const createMockAgentPolicyService = (): jest.Mocked => { return { getAgentStatusById: jest.fn(), + getAgentStatusForAgentPolicy: jest.fn(), authenticateAgentWithAccessToken: jest.fn(), getAgent: jest.fn(), listAgents: jest.fn(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 0ab102d91cd4..9991f4ee2098 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -15,8 +15,6 @@ import type { PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, - RequestHandlerContext, - KibanaRequest, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -29,7 +27,7 @@ import type { } from '../../encrypted_saved_objects/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import type { FleetConfigType, NewPackagePolicy, UpdatePackagePolicy } from '../common'; +import type { FleetConfigType } from '../common'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; import type { CloudSetup } from '../../cloud/server'; @@ -57,6 +55,8 @@ import { registerAppRoutes, registerPreconfigurationRoutes, } from './routes'; + +import type { ExternalCallback } from './types'; import type { ESIndexPatternService, AgentService, @@ -72,6 +72,7 @@ import { } from './services'; import { getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, getAgentsByKuery, getAgentById, @@ -126,29 +127,6 @@ const allSavedObjectTypes = [ PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ]; -/** - * Callbacks supported by the Fleet plugin - */ -export type ExternalCallback = - | [ - 'packagePolicyCreate', - ( - newPackagePolicy: NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest - ) => Promise - ] - | [ - 'packagePolicyUpdate', - ( - newPackagePolicy: UpdatePackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest - ) => Promise - ]; - -export type ExternalCallbacksStorage = Map>; - /** * Describes public Fleet plugin contract returned at the `startup` stage. */ @@ -309,6 +287,7 @@ export class FleetPlugin getAgent: getAgentById, listAgents: getAgentsByKuery, getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, }, agentPolicyService: { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 72a7f4e35ddf..fd4721309eeb 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -202,13 +202,11 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; try { // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( - soClient, esClient, request.query.policyId, request.query.kuery diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index c0b4eeecdfe8..fb28c7e2f515 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -12,7 +12,11 @@ import type { IRouter, RequestHandler, RouteConfig } from 'kibana/server'; import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants'; import { appContextService, packagePolicyService } from '../../services'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; -import type { PackagePolicyServiceInterface, ExternalCallback } from '../..'; +import type { + PackagePolicyServiceInterface, + PostPackagePolicyCreateCallback, + PutPackagePolicyUpdateCallback, +} from '../..'; import type { CreatePackagePolicyRequestSchema } from '../../types/rest_spec'; import { registerRoutes } from './index'; @@ -58,8 +62,11 @@ jest.mock('../../services/package_policy', (): { list: jest.fn(), listIds: jest.fn(), update: jest.fn(), - runExternalCallbacks: jest.fn((callbackType, newPackagePolicy, context, request) => - Promise.resolve(newPackagePolicy) + // @ts-ignore + runExternalCallbacks: jest.fn((callbackType, packagePolicy, context, request) => + callbackType === 'postPackagePolicyDelete' + ? Promise.resolve(undefined) + : Promise.resolve(packagePolicy) ), upgrade: jest.fn(), getUpgradeDryRunDiff: jest.fn(), @@ -132,45 +139,49 @@ describe('When calling package policy', () => { const callbackCallingOrder: string[] = []; // Callback one adds an input that includes a `config` property - const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { - callbackCallingOrder.push('one'); - const newDs = { - ...ds, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - one: { - value: 'inserted by callbackOne', + const callbackOne: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( + async (ds) => { + callbackCallingOrder.push('one'); + const newDs = { + ...ds, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, }, }, - }, - ], - }; - return newDs; - }); + ], + }; + return newDs; + } + ); // Callback two adds an additional `input[0].config` property - const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { - callbackCallingOrder.push('two'); - const newDs = { - ...ds, - inputs: [ - { - ...ds.inputs[0], - config: { - ...ds.inputs[0].config, - two: { - value: 'inserted by callbackTwo', + const callbackTwo: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( + async (ds) => { + callbackCallingOrder.push('two'); + const newDs = { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + two: { + value: 'inserted by callbackTwo', + }, }, }, - }, - ], - }; - return newDs; - }); + ], + }; + return newDs; + } + ); beforeEach(() => { appContextService.addExternalCallback('packagePolicyCreate', callbackOne); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 78785a7b2f01..88e200e90467 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -171,6 +171,17 @@ export const deletePackagePolicyHandler: RequestHandler< request.body.packagePolicyIds, { user, force: request.body.force } ); + try { + await packagePolicyService.runExternalCallbacks( + 'postPackagePolicyDelete', + body, + context, + request + ); + } catch (error) { + const logger = appContextService.getLogger(); + logger.error(`An error occurred executing external callback: ${error}`); + } return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index e3ecdcda20d2..d3cccd4c07f3 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -74,6 +74,7 @@ const MONITORING_DATASETS = [ 'elastic_agent.packetbeat', 'elastic_agent.endpoint_security', 'elastic_agent.auditbeat', + 'elastic_agent.heartbeat', ]; class AgentPolicyService { diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 26cca630f958..cd8f9b95599b 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import pMap from 'p-map'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -49,7 +49,6 @@ function joinKuerys(...kuerys: Array) { } export async function getAgentStatusForAgentPolicy( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicyId?: string, filterKuery?: string diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 82ec0aad5265..1fb34a9a399e 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -21,9 +21,17 @@ import type { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; + import type { SecurityPluginStart } from '../../../security/server'; import type { FleetConfigType } from '../../common'; -import type { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin'; +import type { + ExternalCallback, + ExternalCallbacksStorage, + PostPackagePolicyCreateCallback, + PostPackagePolicyDeleteCallback, + PutPackagePolicyUpdateCallback, +} from '../types'; +import type { FleetAppContext } from '../plugin'; import type { CloudSetup } from '../../../cloud/server'; class AppContextService { @@ -165,9 +173,25 @@ class AppContextService { this.externalCallbacks.get(type)!.add(callback); } - public getExternalCallbacks(type: ExternalCallback[0]) { + public getExternalCallbacks( + type: T + ): + | Set< + T extends 'packagePolicyCreate' + ? PostPackagePolicyCreateCallback + : T extends 'postPackagePolicyDelete' + ? PostPackagePolicyDeleteCallback + : PutPackagePolicyUpdateCallback + > + | undefined { if (this.externalCallbacks) { - return this.externalCallbacks.get(type); + return this.externalCallbacks.get(type) as Set< + T extends 'packagePolicyCreate' + ? PostPackagePolicyCreateCallback + : T extends 'postPackagePolicyDelete' + ? PostPackagePolicyDeleteCallback + : PutPackagePolicyUpdateCallback + >; } } } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index a6aa87c5ed0f..46750105900d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -14,7 +14,11 @@ import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; -import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants'; +import { + FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_ID, + FLEET_FINAL_PIPELINE_VERSION, +} from '../../../../constants'; import { deletePipelineRefs } from './remove'; @@ -195,7 +199,12 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc esClientRequestOptions ); - if (res.statusCode === 404) { + const installedVersion = res?.body[FLEET_FINAL_PIPELINE_ID]?.version; + if ( + res.statusCode === 404 || + !installedVersion || + installedVersion < FLEET_FINAL_PIPELINE_VERSION + ) { await installPipeline({ esClient, pipeline: { diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index f4355320c5a6..ecef04af6b11 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -10,6 +10,8 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/ser import type { AgentStatus, Agent } from '../types'; +import type { GetAgentStatusResponse } from '../../common'; + import type { getAgentById, getAgentsByKuery } from './agents'; import type { agentPolicyService } from './agent_policy'; import * as settingsService from './settings'; @@ -56,6 +58,14 @@ export interface AgentService { * Return the status by the Agent's id */ getAgentStatusById(esClient: ElasticsearchClient, agentId: string): Promise; + /** + * Return the status by the Agent's Policy id + */ + getAgentStatusForAgentPolicy( + esClient: ElasticsearchClient, + agentPolicyId?: string, + filterKuery?: string + ): Promise; /** * List agents */ diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index b3626a83c41d..66128c7e6c3e 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -16,7 +16,7 @@ import type { KibanaRequest } from 'kibana/server'; import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; -import type { ExternalCallback } from '..'; +import type { PutPackagePolicyUpdateCallback, PostPackagePolicyCreateCallback } from '..'; import { createAppContextStartContractMock, xpackMocks } from '../mocks'; @@ -105,6 +105,8 @@ jest.mock('./agent_policy', () => { }; }); +type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; + describe('Package policy service', () => { describe('compilePackagePolicyInputs', () => { it('should work with config variables from the stream', async () => { @@ -835,7 +837,7 @@ describe('Package policy service', () => { const callbackCallingOrder: string[] = []; // Callback one adds an input that includes a `config` property - const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackOne: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('one'); return { ...ds, @@ -855,7 +857,7 @@ describe('Package policy service', () => { }); // Callback two adds an additional `input[0].config` property - const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackTwo: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('two'); return { ...ds, @@ -886,12 +888,12 @@ describe('Package policy service', () => { }); it('should call external callbacks in expected order', async () => { - const callbackA: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackA: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('a'); return ds; }); - const callbackB: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackB: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('b'); return ds; }); @@ -927,12 +929,12 @@ describe('Package policy service', () => { }); describe('with a callback that throws an exception', () => { - const callbackThree: ExternalCallback[1] = jest.fn(async () => { + const callbackThree: CombinedExternalCallback = jest.fn(async () => { callbackCallingOrder.push('three'); throw new Error('callbackThree threw error on purpose'); }); - const callbackFour: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackFour: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('four'); return { ...ds, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 8bfb9971ae07..573e1847f8eb 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -27,7 +27,6 @@ import type { UpgradePackagePolicyResponse, PackagePolicyInput, NewPackagePolicyInput, - NewPackagePolicyInputStream, PackagePolicyConfigRecordEntry, PackagePolicyInputStream, PackageInfo, @@ -425,6 +424,11 @@ class PackagePolicyService { id, name: packagePolicy.name, success: true, + package: { + name: packagePolicy.name, + title: '', + version: packagePolicy.version || '', + }, }); } catch (error) { result.push({ @@ -516,7 +520,13 @@ class PackagePolicyService { updatePackagePolicy.inputs as PackagePolicyInput[] ); - await this.update(soClient, esClient, id, updatePackagePolicy, options); + await this.update( + soClient, + esClient, + id, + omit(updatePackagePolicy, 'missingVars'), + options + ); result.push({ id, name: packagePolicy.name, @@ -620,29 +630,47 @@ class PackagePolicyService { return Promise.all(inputsPromises); } + public async runExternalCallbacks( + externalCallbackType: A, + packagePolicy: NewPackagePolicy | DeletePackagePoliciesResponse, + context: RequestHandlerContext, + request: KibanaRequest + ): Promise; public async runExternalCallbacks( externalCallbackType: ExternalCallback[0], - newPackagePolicy: NewPackagePolicy, + packagePolicy: NewPackagePolicy | DeletePackagePoliciesResponse, context: RequestHandlerContext, request: KibanaRequest - ): Promise { - let newData = newPackagePolicy; - - const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); - if (externalCallbacks && externalCallbacks.size > 0) { - let updatedNewData: NewPackagePolicy = newData; - for (const callback of externalCallbacks) { - const result = await callback(updatedNewData, context, request); - if (externalCallbackType === 'packagePolicyCreate') { - updatedNewData = NewPackagePolicySchema.validate(result); - } else if (externalCallbackType === 'packagePolicyUpdate') { - updatedNewData = UpdatePackagePolicySchema.validate(result); + ): Promise { + if (externalCallbackType === 'postPackagePolicyDelete') { + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + if (externalCallbacks && externalCallbacks.size > 0) { + for (const callback of externalCallbacks) { + if (Array.isArray(packagePolicy)) { + await callback(packagePolicy, context, request); + } } } + } else { + if (!Array.isArray(packagePolicy)) { + let newData = packagePolicy; + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewData = newData; + for (const callback of externalCallbacks) { + const result = await callback(updatedNewData, context, request); + if (externalCallbackType === 'packagePolicyCreate') { + updatedNewData = NewPackagePolicySchema.validate(result); + } else if (externalCallbackType === 'packagePolicyUpdate') { + updatedNewData = UpdatePackagePolicySchema.validate(result); + } + } - newData = updatedNewData; + newData = updatedNewData; + } + return newData; + } } - return newData; } } @@ -829,9 +857,10 @@ export function overridePackageInputs( const inputs = [...basePackagePolicy.inputs]; const packageName = basePackagePolicy.package!.name; const errors = []; + let responseMissingVars: string[] = []; for (const override of inputsOverride) { - const originalInput = inputs.find((i) => i.type === override.type); + let originalInput = inputs.find((i) => i.type === override.type); if (!originalInput) { const e = { error: new Error( @@ -860,7 +889,9 @@ export function overridePackageInputs( if (override.vars) { try { - deepMergeVars(override, originalInput); + const { result, missingVars } = deepMergeVars(override, originalInput); + originalInput = result; + responseMissingVars = [...responseMissingVars, ...missingVars]; } catch (e) { const varName = e.message; const err = { @@ -888,7 +919,7 @@ export function overridePackageInputs( if (override.streams) { for (const stream of override.streams) { - const originalStream = originalInput.streams.find( + let originalStream = originalInput?.streams.find( (s) => s.data_stream.dataset === stream.data_stream.dataset ); if (!originalStream) { @@ -920,7 +951,9 @@ export function overridePackageInputs( if (stream.vars) { try { - deepMergeVars(stream as InputsOverride, originalStream); + const { result, missingVars } = deepMergeVars(stream as InputsOverride, originalStream); + originalStream = result; + responseMissingVars = [...responseMissingVars, ...missingVars]; } catch (e) { const varName = e.message; const streamSet = stream.data_stream.dataset; @@ -951,25 +984,33 @@ export function overridePackageInputs( } } - if (dryRun && errors.length) return { ...basePackagePolicy, inputs, errors }; - return { ...basePackagePolicy, inputs }; + if (dryRun && errors.length) { + return { ...basePackagePolicy, inputs, errors, missingVars: responseMissingVars }; + } + + return { ...basePackagePolicy, inputs, missingVars: responseMissingVars }; } -function deepMergeVars( - override: NewPackagePolicyInput | InputsOverride, - original: NewPackagePolicyInput | NewPackagePolicyInputStream -) { +function deepMergeVars(override: any, original: any): { result: any; missingVars: string[] } { + const result = { ...original }; + const missingVars: string[] = []; + const overrideVars = Array.isArray(override.vars) ? override.vars : Object.entries(override.vars!).map(([key, rest]) => ({ name: key, - ...rest, + ...(rest as any), })); + for (const { name, ...val } of overrideVars) { - if (!original.vars || !Reflect.has(original.vars, name)) { - throw new Error(name); + if (!original.vars || !(name in original.vars)) { + missingVars.push(name); + continue; } + const originalVar = original.vars[name]; - Reflect.set(original.vars, name, { ...originalVar, ...val }); + result[name] = { ...originalVar, ...val }; } + + return { result, missingVars }; } diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts new file mode 100644 index 000000000000..bca9cc016f82 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest, RequestHandlerContext } from 'kibana/server'; + +import type { + DeletePackagePoliciesResponse, + NewPackagePolicy, + UpdatePackagePolicy, +} from '../../common'; + +export type PostPackagePolicyDeleteCallback = ( + deletedPackagePolicies: DeletePackagePoliciesResponse, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type PostPackagePolicyCreateCallback = ( + newPackagePolicy: NewPackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type PutPackagePolicyUpdateCallback = ( + updatePackagePolicy: UpdatePackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback]; +export type ExternalCallbackDelete = ['postPackagePolicyDelete', PostPackagePolicyDeleteCallback]; +export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback]; + +/** + * Callbacks supported by the Fleet plugin + */ +export type ExternalCallback = + | ExternalCallbackCreate + | ExternalCallbackDelete + | ExternalCallbackUpdate; + +export type ExternalCallbacksStorage = Map>; diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index e32b462d11ca..f686b969fd03 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -94,3 +94,4 @@ export interface BulkActionResult { export * from './models'; export * from './rest_spec'; +export * from './extensions'; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index a20d82de3c85..5002bf289387 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search/common/types.ts b/x-pack/plugins/global_search/common/types.ts index a910cc5cd3ae..fabae7ea01e8 100644 --- a/x-pack/plugins/global_search/common/types.ts +++ b/x-pack/plugins/global_search/common/types.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { Serializable } from 'src/core/types'; +import { Serializable } from '@kbn/utility-types'; /** * Options provided to {@link GlobalSearchResultProvider | a result provider}'s `find` method. diff --git a/x-pack/plugins/global_search/tsconfig.json b/x-pack/plugins/global_search/tsconfig.json index 2d05328f445d..6a0385e5c080 100644 --- a/x-pack/plugins/global_search/tsconfig.json +++ b/x-pack/plugins/global_search/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search_bar/tsconfig.json b/x-pack/plugins/global_search_bar/tsconfig.json index 266eecc35c84..04464a3c0820 100644 --- a/x-pack/plugins/global_search_bar/tsconfig.json +++ b/x-pack/plugins/global_search_bar/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search_providers/tsconfig.json b/x-pack/plugins/global_search_providers/tsconfig.json index f2759954a684..4ce15f6d4468 100644 --- a/x-pack/plugins/global_search_providers/tsconfig.json +++ b/x-pack/plugins/global_search_providers/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/graph/public/types/workspace_state.ts b/x-pack/plugins/graph/public/types/workspace_state.ts index e511a2eb5c77..86f05376b952 100644 --- a/x-pack/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/plugins/graph/public/types/workspace_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FontawesomeIcon } from '../helpers/style_choices'; import { WorkspaceField, AdvancedSettings } from './app_state'; diff --git a/x-pack/plugins/graph/tsconfig.json b/x-pack/plugins/graph/tsconfig.json index 741c603e3aae..d655f28c4e46 100644 --- a/x-pack/plugins/graph/tsconfig.json +++ b/x-pack/plugins/graph/tsconfig.json @@ -1,30 +1,29 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "*.ts", - "common/**/*", - "public/**/*", - "server/**/*", - "../../../typings/**/*", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../features/tsconfig.json"}, - { "path": "../../../src/plugins/data/tsconfig.json"}, - { "path": "../../../src/plugins/navigation/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"}, - { "path": "../../../src/plugins/home/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" } - ] - } \ No newline at end of file + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../features/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"}, + { "path": "../../../src/plugins/home/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/grokdebugger/tsconfig.json b/x-pack/plugins/grokdebugger/tsconfig.json index 51d2d0b6db0e..aefb15f74c7b 100644 --- a/x-pack/plugins/grokdebugger/tsconfig.json +++ b/x-pack/plugins/grokdebugger/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts index 025946a095a6..4df7cf85ddeb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/locator.ts +++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ManagementAppLocator } from 'src/plugins/management/common'; import { LocatorDefinition } from '../../../../src/plugins/share/public/'; import { @@ -17,7 +17,7 @@ import { PLUGIN } from '../common/constants'; export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; -export interface IlmLocatorParams extends SerializableState { +export interface IlmLocatorParams extends SerializableRecord { page: 'policies_list' | 'policy_edit' | 'policy_create'; policyName?: string; } diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index 75bd775a3674..d3a342e11021 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json index 81a96a77cef8..120e58c2850c 100644 --- a/x-pack/plugins/index_management/tsconfig.json +++ b/x-pack/plugins/index_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/infra/common/typed_json.ts b/x-pack/plugins/infra/common/typed_json.ts index 44409ab433a6..fee846b437a7 100644 --- a/x-pack/plugins/infra/common/typed_json.ts +++ b/x-pack/plugins/infra/common/typed_json.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { JsonArray, JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonObject, JsonValue } from '@kbn/utility-types'; export { JsonArray, JsonObject, JsonValue }; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index 87419a9bfbe7..b241147c8d67 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -195,7 +195,11 @@ This will show a list of log entries between the specified timestamps. ## Query log entries -You might want to show specific log entries in your plugin. Maybe you want to show log lines from a specific host, or for an AMP trace. The component has a `query` prop that accepts valid KQL expressions. +You might want to show specific log entries in your plugin. Maybe you want to show log lines from a specific host, or for an AMP trace. The LogStream component supports both `query` and `filters`, and these are the standard `es-query` types. + +### Query + +The component has a `query` prop that accepts a valid es-query `query`. You can either supply this with a `language` and `query` property, or you can just supply a string which is a shortcut for KQL expressions. ```tsx ``` +### Filters + +The component also has a `filters` prop that accepts valid es-query `filters`. This example would specifiy that we want the `message` field to exist: + +```tsx + +``` + ## Center the view on a specific entry By default the component will load at the bottom of the list, showing the newest entries. You can change the rendering point with the `center` prop. The prop takes a [`LogEntriesCursor`](https://github.com/elastic/kibana/blob/0a6c748cc837c016901f69ff05d81395aa2d41c8/x-pack/plugins/infra/common/http_api/log_entries/common.ts#L9-L13). @@ -431,3 +460,7 @@ class MyPlugin { endTimestamp={...} /> ``` + +### Setting component height + +It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume. diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index ff9b749911c8..2698e975cebc 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { buildEsQuery, Query, Filter } from '@kbn/es-query'; import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; -import { JsonValue } from '@kbn/common-utils'; -import { DataPublicPluginStart, esQuery, Filter } from '../../../../../../src/plugins/data/public'; +import { JsonValue } from '@kbn/utility-types'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { LogEntryCursor } from '../../../common/log_entry'; @@ -18,7 +19,6 @@ import { BuiltEsQuery, useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; -import { Query } from '../../../../../../src/plugins/data/common'; import { LogStreamErrorBoundary } from './log_stream_error_boundary'; interface LogStreamPluginDeps { @@ -123,9 +123,9 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const parsedQuery = useMemo(() => { if (typeof query === 'object' && 'bool' in query) { - return mergeBoolQueries(query, esQuery.buildEsQuery(derivedIndexPattern, [], filters ?? [])); + return mergeBoolQueries(query, buildEsQuery(derivedIndexPattern, [], filters ?? [])); } else { - return esQuery.buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []); + return buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []); } }, [derivedIndexPattern, filters, query]); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx index 9cffef270219..e52d302a9193 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx @@ -7,7 +7,7 @@ import stringify from 'json-stable-stringify'; import React from 'react'; -import { JsonArray, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index 33e81756552d..d8021aa0279d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogColumn } from '../../../../common/log_entry'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; diff --git a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx index a6adc716e02f..ff4a24f1498a 100644 --- a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx +++ b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx @@ -6,7 +6,7 @@ */ import { ReactNode } from 'react'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; /** * Interface for common configuration properties, regardless of the column type. 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 f33bcd2fcab0..3cd435ab0f6e 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 @@ -8,7 +8,7 @@ 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'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { RouteConfig, RouteMethod } from '../../../../../../../src/core/server'; import { PluginSetup as DataPluginSetup, diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 9f2e9e2713bb..4ad2fa656f9b 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -11,7 +11,7 @@ import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; import { compact } from 'lodash'; -import { JsonArray } from '@kbn/common-utils'; +import { JsonArray } from '@kbn/utility-types'; import type { InfraPluginRequestHandlerContext } from '../../../types'; import { LogEntriesAdapter, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index f8268570710f..f6be310d79ed 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import type { InfraPluginRequestHandlerContext } from '../../../types'; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts index 33060f428b7f..1f8760993c86 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { InventoryItemType, MetricsUIAggregation, diff --git a/x-pack/plugins/infra/server/services/log_entries/message/message.ts b/x-pack/plugins/infra/server/services/log_entries/message/message.ts index 2deee584f518..fc547126b3b4 100644 --- a/x-pack/plugins/infra/server/services/log_entries/message/message.ts +++ b/x-pack/plugins/infra/server/services/log_entries/message/message.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonArray, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { LogMessagePart } from '../../../../common/log_entry'; import { LogMessageFormattingCondition, diff --git a/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts b/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts index 56d1b38e7e39..65229a747e5e 100644 --- a/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts +++ b/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; export interface LogMessageFormattingRule { when: LogMessageFormattingCondition; diff --git a/x-pack/plugins/infra/server/utils/serialized_query.ts b/x-pack/plugins/infra/server/utils/serialized_query.ts index 4169e123d853..b3b2569528ae 100644 --- a/x-pack/plugins/infra/server/utils/serialized_query.ts +++ b/x-pack/plugins/infra/server/utils/serialized_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export const parseFilterQuery = ( filterQuery: string | null | undefined diff --git a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts index 2482694474b0..7dcda66e1bb9 100644 --- a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts +++ b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts @@ -7,7 +7,7 @@ import * as rt from 'io-ts'; import stringify from 'json-stable-stringify'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { jsonValueRT } from '../../common/typed_json'; import { SearchStrategyError } from '../../common/search_strategies/common/errors'; import { ShardFailure } from './elasticsearch_runtime_types'; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 765af7974a2f..a9739bdfdedc 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts index bfcc2f0fc9ce..e4d6bf353949 100644 --- a/x-pack/plugins/ingest_pipelines/public/locator.ts +++ b/x-pack/plugins/ingest_pipelines/public/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ManagementAppLocator } from 'src/plugins/management/common'; import { LocatorPublic, @@ -27,7 +27,7 @@ export enum INGEST_PIPELINES_PAGES { CLONE = 'pipeline_clone', } -interface IngestPipelinesBaseParams extends SerializableState { +interface IngestPipelinesBaseParams extends SerializableRecord { pipelineId: string; } export interface IngestPipelinesListParams extends Partial { diff --git a/x-pack/plugins/ingest_pipelines/tsconfig.json b/x-pack/plugins/ingest_pipelines/tsconfig.json index a248bc9f337f..de9a8362e8c6 100644 --- a/x-pack/plugins/ingest_pipelines/tsconfig.json +++ b/x-pack/plugins/ingest_pipelines/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts index d2db63a01793..32f6c1c08954 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import type { + ExecutionContext, DatatableColumnMeta, ExpressionFunctionDefinition, } from '../../../../../../src/plugins/expressions/common'; @@ -46,15 +47,13 @@ function isRange(meta: { params?: { id?: string } } | undefined) { return meta?.params?.id === 'range'; } -export const getDatatable = ({ - formatFactory, -}: { - formatFactory: FormatFactory; -}): ExpressionFunctionDefinition< +export const getDatatable = ( + getFormatFactory: (context: ExecutionContext) => FormatFactory | Promise +): ExpressionFunctionDefinition< 'lens_datatable', LensMultiTable, DatatableArgs, - DatatableRender + Promise > => ({ name: 'lens_datatable', type: 'render', @@ -87,12 +86,13 @@ export const getDatatable = ({ help: '', }, }, - fn(data, args, context) { + async fn(data, args, context) { let untransposedData: LensMultiTable | undefined; // do the sorting at this level to propagate it also at CSV download const [firstTable] = Object.values(data.tables); const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); const formatters: Record> = {}; + const formatFactory = await getFormatFactory(context); firstTable.columns.forEach((column) => { formatters[column.id] = formatFactory(column.meta?.params); diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 6a3e0f40c48f..9565bf57a315 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -8,6 +8,7 @@ "data", "charts", "expressions", + "fieldFormats", "inspector", "navigation", "urlForwarding", diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index c1984991b04d..c26cce3317cf 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -13,13 +13,7 @@ import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; -import { - createMockDatasource, - createMockVisualization, - DatasourceMock, - makeDefaultServices, - mountWithProvider, -} from '../mocks'; +import { visualizationMap, datasourceMap, makeDefaultServices, mountWithProvider } from '../mocks'; import { I18nProvider } from '@kbn/i18n/react'; import { SavedObjectSaveModal, @@ -71,29 +65,6 @@ describe('Lens App', () => { let defaultDoc: Document; let defaultSavedObjectId: string; - const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); - const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); - const datasourceMap = { - testDatasource2: mockDatasource2, - testDatasource: mockDatasource, - }; - - const mockVisualization = { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - groupLabel: 'testVisGroup', - }, - ], - }; - const visualizationMap = { - testVis: mockVisualization, - }; - function createMockFrame(): jest.Mocked { return { EditorFrameContainer: jest.fn((props: EditorFrameProps) =>
), @@ -1082,11 +1053,12 @@ describe('Lens App', () => { }); it('updates the state if session id changes from the outside', async () => { - const services = makeDefaultServices(sessionIdSubject); + const sessionIdS = new Subject(); + const services = makeDefaultServices(sessionIdS); const { lensStore } = await mountWith({ props: undefined, services }); act(() => { - sessionIdSubject.next('new-session-id'); + sessionIdS.next('new-session-id'); }); await act(async () => { await new Promise((r) => setTimeout(r, 0)); @@ -1181,7 +1153,7 @@ describe('Lens App', () => { ...defaultDoc, state: { ...defaultDoc.state, - datasourceStates: { testDatasource: '' }, + datasourceStates: { testDatasource: {} }, visualization: {}, }, }, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 8faee830d52b..4b956768265e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -29,13 +29,13 @@ import { useLensDispatch, LensAppState, DispatchSetState, + selectSavedObjectFormat, } from '../state_management'; import { SaveModalContainer, getLastKnownDocWithoutPinnedFilters, runSaveLensVisualization, } from './save_modal_container'; -import { getSavedObjectFormat } from '../utils'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -79,11 +79,6 @@ export function App({ ); const { - datasourceStates, - visualization, - filters, - query, - activeDatasourceId, persistedDoc, isLinkedToOriginatingApp, searchSessionId, @@ -91,52 +86,20 @@ export function App({ isSaveable, } = useLensSelector((state) => state.lens); + const currentDoc = useLensSelector((state) => + selectSavedObjectFormat(state, datasourceMap, visualizationMap) + ); + // Used to show a popover that guides the user towards changing the date range when no data is available. const [indicateNoData, setIndicateNoData] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); const [lastKnownDoc, setLastKnownDoc] = useState(undefined); useEffect(() => { - const activeVisualization = visualization.activeId && visualizationMap[visualization.activeId]; - const activeDatasource = - activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading - ? datasourceMap[activeDatasourceId] - : undefined; - - if (!activeDatasource || !activeVisualization || !visualization.state) { - return; + if (currentDoc) { + setLastKnownDoc(currentDoc); } - setLastKnownDoc( - // todo: that should be redux store selector - getSavedObjectFormat({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - visualization, - filters, - query, - title: persistedDoc?.title || '', - description: persistedDoc?.description, - persistedId: persistedDoc?.savedObjectId, - }) - ); - }, [ - persistedDoc?.title, - persistedDoc?.description, - persistedDoc?.savedObjectId, - datasourceStates, - visualization, - filters, - query, - activeDatasourceId, - datasourceMap, - visualizationMap, - ]); + }, [currentDoc]); const showNoDataPopover = useCallback(() => { setIndicateNoData(true); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 5f348d2a88ea..f777d053b314 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -141,6 +141,7 @@ export const LensTopNavMenu = ({ }: LensTopNavMenuProps) => { const { data, + fieldFormats, navigation, uiSettings, application, @@ -232,7 +233,7 @@ export const LensTopNavMenu = ({ if (formulaDetected) { return i18n.translate('xpack.lens.app.downloadButtonFormulasWarning', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', }); } } @@ -255,7 +256,7 @@ export const LensTopNavMenu = ({ content: exporters.datatableToCSV(datatable, { csvSeparator: uiSettings.get('csv:separator', ','), quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, + formatFactory: fieldFormats.deserialize, escapeFormulaValues: false, }), type: exporters.CSV_MIME_TYPE, @@ -305,7 +306,7 @@ export const LensTopNavMenu = ({ activeData, attributeService, dashboardFeatureFlag.allowByValueEmbeddables, - data.fieldFormats.deserialize, + fieldFormats.deserialize, getIsByValueMode, initialInput, isLinkedToOriginatingApp, diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 55f3e7463754..5a783bc4180d 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -46,7 +46,14 @@ export async function getLensServices( startDependencies: LensPluginStartDependencies, attributeService: () => Promise ): Promise { - const { data, navigation, embeddable, savedObjectsTagging, usageCollection } = startDependencies; + const { + data, + navigation, + embeddable, + savedObjectsTagging, + usageCollection, + fieldFormats, + } = startDependencies; const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); @@ -56,6 +63,7 @@ export async function getLensServices( data, storage, navigation, + fieldFormats, stateTransfer, usageCollection, savedObjectsTagging, diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index c482b56b7030..b6530b90ac3f 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { History } from 'history'; -import { OnSaveProps } from 'src/plugins/saved_objects/public'; -import { +import type { History } from 'history'; +import type { OnSaveProps } from 'src/plugins/saved_objects/public'; +import type { ApplicationStart, AppMountParameters, ChromeStart, @@ -17,25 +17,27 @@ import { OverlayStart, SavedObjectsStart, } from '../../../../../src/core/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { UsageCollectionStart } from '../../../../../src/plugins/usage_collection/public'; -import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; -import { LensEmbeddableInput } from '../embeddable/embeddable'; -import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; -import { LensAttributeService } from '../lens_attribute_service'; -import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; -import { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; +import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import type { UsageCollectionStart } from '../../../../../src/plugins/usage_collection/public'; +import type { DashboardStart } from '../../../../../src/plugins/dashboard/public'; +import type { LensEmbeddableInput } from '../embeddable/embeddable'; +import type { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; +import type { LensAttributeService } from '../lens_attribute_service'; +import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; +import type { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; import { VisualizeFieldContext, ACTION_VISUALIZE_LENS_FIELD, } from '../../../../../src/plugins/ui_actions/public'; -import { +import type { EmbeddableEditorState, EmbeddableStateTransfer, } from '../../../../../src/plugins/embeddable/public'; -import { DatasourceMap, EditorFrameInstance, VisualizationMap } from '../types'; -import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public'; +import type { DatasourceMap, EditorFrameInstance, VisualizationMap } from '../types'; +import type { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public'; +import type { FieldFormatsStart } from '../../../../../src/plugins/field_formats/public'; + export interface RedirectToOriginProps { input?: LensEmbeddableInput; isCopied?: boolean; @@ -97,6 +99,7 @@ export interface LensAppServices { overlays: OverlayStart; storage: IStorageWrapper; dashboard: DashboardStart; + fieldFormats: FieldFormatsStart; data: DataPublicPluginStart; uiSettings: IUiSettingsClient; application: ApplicationStart; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index 163971c4ba9f..b2a25cba329d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { DatatableProps } from '../../common/expressions'; +import type { DatatableProps } from '../../common/expressions'; import type { LensMultiTable } from '../../common'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; -import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common'; +import type { FormatFactory } from '../../common'; import { getDatatable } from './expression'; function sampleArgs() { @@ -83,9 +83,9 @@ function sampleArgs() { describe('datatable_expression', () => { describe('datatable renders', () => { - test('it renders with the specified data and args', () => { + test('it renders with the specified data and args', async () => { const { data, args } = sampleArgs(); - const result = getDatatable({ formatFactory: (x) => x as IFieldFormat }).fn( + const result = await getDatatable(() => Promise.resolve((() => {}) as FormatFactory)).fn( data, args, createMockExecutionContext() diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index b4f37faf0bc0..3349f229a604 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -17,7 +17,7 @@ interface DatatableVisualizationPluginStartPlugins { } export interface DatatableVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } @@ -37,13 +37,12 @@ export class DatatableVisualization { getDatatableVisualization, } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - const resolvedFormatFactory = await formatFactory; expressions.registerFunction(() => datatableColumn); - expressions.registerFunction(() => getDatatable({ formatFactory: resolvedFormatFactory })); + expressions.registerFunction(() => getDatatable(() => formatFactory)); expressions.registerRenderer(() => getDatatableRenderer({ - formatFactory: resolvedFormatFactory, + formatFactory, getType: core .getStartServices() .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 552f0f94a67d..1e4b1cfa6069 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -22,12 +22,6 @@ function mockFrame(): FramePublicAPI { return { ...createMockFramePublicAPI(), datasourceLayers: {}, - query: { query: '', language: 'lucene' }, - dateRange: { - fromDate: 'now-7d', - toDate: 'now', - }, - filters: [], }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 8a5d385b5be0..804f73b5d5fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -24,24 +24,32 @@ import { updateDatasourceState, updateVisualizationState, setToggleFullscreen, + useLensSelector, + selectVisualization, } from '../../../state_management'; export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - return props.activeVisualization && props.visualizationState ? ( - + const visualization = useLensSelector(selectVisualization); + const activeVisualization = visualization.activeId + ? props.visualizationMap[visualization.activeId] + : null; + + return activeVisualization && visualization.state ? ( + ) : null; }); export function LayerPanels( props: ConfigPanelWrapperProps & { - activeDatasourceId: string; activeVisualization: Visualization; } ) { - const { activeVisualization, visualizationState, activeDatasourceId, datasourceMap } = props; + const { activeVisualization, datasourceMap } = props; + const { activeDatasourceId, visualization } = useLensSelector((state) => state.lens); + const dispatchLens = useLensDispatch(); - const layerIds = activeVisualization.getLayerIds(visualizationState); + const layerIds = activeVisualization.getLayerIds(visualization.state); const { setNextFocusedId: setNextFocusedLayerId, removeRef: removeLayerRef, @@ -139,7 +147,7 @@ export function LayerPanels( key={layerId} layerId={layerId} layerIndex={layerIndex} - visualizationState={visualizationState} + visualizationState={visualization.state} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateDatasourceAsync={updateDatasourceAsync} @@ -187,7 +195,7 @@ export function LayerPanels( /> ) : null )} - {activeVisualization.appendLayer && visualizationState && ( + {activeVisualization.appendLayer && visualization.state && ( id, trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], + activeDatasource: datasourceMap[activeDatasourceId!], state, }), }) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 3bb5fca2141a..12f27b5bfba1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiFormRow } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test/jest'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; import { ChildDragDropProvider, DragDrop } from '../../../drag_drop'; @@ -19,6 +18,7 @@ import { createMockFramePublicAPI, createMockDatasource, DatasourceMock, + mountWithProvider, } from '../../../mocks'; jest.mock('../../../id_generator'); @@ -65,15 +65,8 @@ describe('LayerPanel', () => { return { layerId: 'first', activeVisualization: mockVisualization, - activeDatasourceId: 'ds1', datasourceMap: { - ds1: mockDatasource, - }, - datasourceStates: { - ds1: { - isLoading: false, - state: 'state', - }, + testDatasource: mockDatasource, }, visualizationState: 'state', updateVisualization: jest.fn(), @@ -120,45 +113,49 @@ describe('LayerPanel', () => { }; mockVisualization.getLayerIds.mockReturnValue(['first']); - mockDatasource = createMockDatasource('ds1'); + mockDatasource = createMockDatasource('testDatasource'); }); describe('layer reset and remove', () => { - it('should show the reset button when single layer', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + it('should show the reset button when single layer', async () => { + const { instance } = await mountWithProvider(); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset layer' ); }); - it('should show the delete button when multiple layers', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + it('should show the delete button when multiple layers', async () => { + const { instance } = await mountWithProvider( + + ); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Delete layer' ); }); - it('should show to reset visualization for visualizations only allowing a single layer', () => { + it('should show to reset visualization for visualizations only allowing a single layer', async () => { const layerPanelAttributes = getDefaultProps(); delete layerPanelAttributes.activeVisualization.removeLayer; - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + const { instance } = await mountWithProvider(); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset visualization' ); }); - it('should call the clear callback', () => { + it('should call the clear callback', async () => { const cb = jest.fn(); - const component = mountWithIntl(); + const { instance } = await mountWithProvider( + + ); act(() => { - component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); }); expect(cb).toHaveBeenCalled(); }); }); describe('single group', () => { - it('should render the non-editable state', () => { + it('should render the non-editable state', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -172,12 +169,12 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); - const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); + const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); - it('should render the group with a way to add a new column', () => { + it('should render the group with a way to add a new column', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -191,12 +188,12 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); - const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); + const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); - it('should render the required warning when only one group is configured', () => { + it('should render the required warning when only one group is configured', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -219,16 +216,16 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); - const group = component + const group = instance .find(EuiFormRow) .findWhere((e) => e.prop('error')?.props?.children === 'Required dimension'); expect(group).toHaveLength(1); }); - it('should render the datasource and visualization panels inside the dimension container', () => { + it('should render the datasource and visualization panels inside the dimension container', async () => { mockVisualization.getConfiguration.mockReturnValueOnce({ groups: [ { @@ -244,18 +241,18 @@ describe('LayerPanel', () => { }); mockVisualization.renderDimensionEditor = jest.fn(); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); - const group = component.find('DimensionContainer').first(); + const group = instance.find('DimensionContainer').first(); const panel: React.ReactElement = group.prop('panel'); expect(panel.props.children).toHaveLength(2); }); - it('should not update the visualization if the datasource is incomplete', () => { + it('should not update the visualization if the datasource is incomplete', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); const updateAll = jest.fn(); const updateDatasourceAsync = jest.fn(); @@ -273,7 +270,7 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( { ); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( expect.any(Element), @@ -319,7 +316,7 @@ describe('LayerPanel', () => { expect(updateAll).toHaveBeenCalled(); }); - it('should remove the dimension when the datasource marks it as removed', () => { + it('should remove the dimension when the datasource marks it as removed', async () => { const updateAll = jest.fn(); const updateDatasource = jest.fn(); @@ -336,38 +333,42 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( , + { + preloadedState: { + datasourceStates: { + testDatasource: { + isLoading: false, + state: { + layers: [ + { + indexPatternId: '1', + columns: { + y: { + operationType: 'moving_average', + references: ['ref'], + }, }, + columnOrder: ['y'], + incompleteColumns: {}, }, - columnOrder: ['y'], - incompleteColumns: {}, - }, - ], + ], + }, }, }, - }} - updateDatasource={updateDatasource} - updateAll={updateAll} - /> + }, + } ); act(() => { - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); }); - component.update(); + instance.update(); expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( expect.any(Element), @@ -399,7 +400,7 @@ describe('LayerPanel', () => { ); }); - it('should keep the DimensionContainer open when configuring a new dimension', () => { + it('should keep the DimensionContainer open when configuring a new dimension', async () => { /** * The ID generation system for new dimensions has been messy before, so * this tests that the ID used in the first render is used to keep the container @@ -436,13 +437,13 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(true); const lastArgs = mockDatasource.renderDimensionEditor.mock.calls[ @@ -459,7 +460,7 @@ describe('LayerPanel', () => { expect(mockVisualization.renderDimensionEditor).toHaveBeenCalled(); }); - it('should close the DimensionContainer when the active visualization changes', () => { + it('should close the DimensionContainer when the active visualization changes', async () => { /** * The ID generation system for new dimensions has been messy before, so * this tests that the ID used in the first render is used to keep the container @@ -495,21 +496,21 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + instance.update(); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(true); act(() => { - component.setProps({ activeVisualization: mockVisualization2 }); + instance.setProps({ activeVisualization: mockVisualization2 }); }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(false); + instance.update(); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(false); }); - it('should only update the state on close when needed', () => { + it('should only update the state on close when needed', async () => { const updateDatasource = jest.fn(); mockVisualization.getConfiguration.mockReturnValue({ groups: [ @@ -524,37 +525,37 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( ); // Close without a state update mockDatasource.updateStateOnCloseDimension = jest.fn(); - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); act(() => { - (component.find('DimensionContainer').first().prop('handleClose') as () => void)(); + (instance.find('DimensionContainer').first().prop('handleClose') as () => void)(); }); - component.update(); + instance.update(); expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled(); expect(updateDatasource).not.toHaveBeenCalled(); // Close with a state update mockDatasource.updateStateOnCloseDimension = jest.fn().mockReturnValue({ newState: true }); - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); act(() => { - (component.find('DimensionContainer').first().prop('handleClose') as () => void)(); + (instance.find('DimensionContainer').first().prop('handleClose') as () => void)(); }); - component.update(); + instance.update(); expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled(); - expect(updateDatasource).toHaveBeenCalledWith('ds1', { newState: true }); + expect(updateDatasource).toHaveBeenCalledWith('testDatasource', { newState: true }); }); }); // This test is more like an integration test, since the layer panel owns all // the coordination between drag and drop describe('drag and drop behavior', () => { - it('should determine if the datasource supports dropping of a field onto empty dimension', () => { + it('should determine if the datasource supports dropping of a field onto empty dimension', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -584,7 +585,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -596,7 +597,7 @@ describe('LayerPanel', () => { }) ); - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroup"] DragDrop .lnsDragDrop') .first(); @@ -610,7 +611,7 @@ describe('LayerPanel', () => { ); }); - it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', () => { + it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -639,7 +640,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -650,10 +651,10 @@ describe('LayerPanel', () => { ); expect( - component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') + instance.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') ).toEqual(undefined); - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroup"] DragDrop') .first() .find('.lnsLayerPanel__dimension'); @@ -664,7 +665,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).not.toHaveBeenCalled(); }); - it('should allow drag to move between groups', () => { + it('should allow drag to move between groups', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); mockVisualization.getConfiguration.mockReturnValue({ @@ -705,7 +706,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -719,7 +720,7 @@ describe('LayerPanel', () => { // Simulate drop on the pre-populated dimension - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') .at(0); dragDropElement.simulate('dragOver'); @@ -734,7 +735,7 @@ describe('LayerPanel', () => { // Simulate drop on the empty dimension - const updatedDragDropElement = component + const updatedDragDropElement = instance .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') .at(2); @@ -749,7 +750,7 @@ describe('LayerPanel', () => { ); }); - it('should reorder when dropping in the same group', () => { + it('should reorder when dropping in the same group', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -775,14 +776,15 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( , + undefined, { attachTo: container } ); act(() => { - component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); + instance.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -790,7 +792,7 @@ describe('LayerPanel', () => { droppedItem: draggingOperation, }) ); - const secondButton = component + const secondButton = instance .find(DragDrop) .at(1) .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') @@ -799,7 +801,7 @@ describe('LayerPanel', () => { expect(focusedEl).toEqual(secondButton); }); - it('should copy when dropping on empty slot in the same group', () => { + it('should copy when dropping on empty slot in the same group', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); mockVisualization.getConfiguration.mockReturnValue({ groups: [ @@ -826,13 +828,13 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( ); act(() => { - component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); + instance.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -843,7 +845,7 @@ describe('LayerPanel', () => { ); }); - it('should call onDrop and update visualization when replacing between compatible groups', () => { + it('should call onDrop and update visualization when replacing between compatible groups', async () => { const mockVis = { ...mockVisualization, removeDimension: jest.fn(), @@ -881,7 +883,7 @@ describe('LayerPanel', () => { mockDatasource.onDrop.mockReturnValue({ deleted: 'a' }); const updateVisualization = jest.fn(); - const component = mountWithIntl( + const { instance } = await mountWithProvider( { ); act(() => { - component.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 879a9ec7aced..d0a6830aa178 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -29,6 +29,12 @@ import { EmptyDimensionButton } from './buttons/empty_dimension_button'; import { DimensionButton } from './buttons/dimension_button'; import { DraggableDimensionButton } from './buttons/draggable_dimension_button'; import { useFocusUpdate } from './use_focus_update'; +import { + useLensSelector, + selectIsFullscreenDatasource, + selectResolvedDateRange, + selectDatasourceStates, +} from '../../../state_management'; const initialActiveDimensionState = { isNew: false, @@ -51,7 +57,6 @@ export function LayerPanel( onRemoveLayer: () => void; registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; toggleFullscreen: () => void; - isFullscreen: boolean; } ) { const [activeDimension, setActiveDimension] = useState( @@ -69,12 +74,14 @@ export function LayerPanel( updateVisualization, updateDatasource, toggleFullscreen, - isFullscreen, updateAll, updateDatasourceAsync, visualizationState, } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + const dateRange = useLensSelector(selectResolvedDateRange); + const datasourceStates = useLensSelector(selectDatasourceStates); + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); useEffect(() => { setActiveDimension(initialActiveDimensionState); @@ -90,12 +97,12 @@ export function LayerPanel( layerId, state: props.visualizationState, frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, + dateRange, activeData: props.framePublicAPI.activeData, }; const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasourceState = datasourceStates[datasourceId].state; const layerDatasourceDropProps = useMemo( () => ({ @@ -113,8 +120,8 @@ export function LayerPanel( const layerDatasourceConfigProps = { ...layerDatasourceDropProps, frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, activeData: props.framePublicAPI.activeData, + dateRange, }; const { groups } = useMemo( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index d9bf678d1916..66a30b0a405e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -11,39 +11,21 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, DatasourceMap, + VisualizationMap, } from '../../../types'; export interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - activeVisualization: Visualization | null; framePublicAPI: FramePublicAPI; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + visualizationMap: VisualizationMap; core: DatasourceDimensionEditorProps['core']; - isFullscreen: boolean; } export interface LayerPanelProps { - activeDatasourceId: string; visualizationState: unknown; datasourceMap: DatasourceMap; activeVisualization: Visualization; framePublicAPI: FramePublicAPI; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; core: DatasourceDimensionEditorProps['core']; - isFullscreen: boolean; } export interface LayerDatasourceDropProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index abfeb647186a..b77d31397343 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -10,7 +10,6 @@ import './data_panel_wrapper.scss'; import React, { useMemo, memo, useContext, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { createSelector } from '@reduxjs/toolkit'; import { NativeRenderer } from '../../native_renderer'; import { DragContext, DragDropIdentifier } from '../../drag_drop'; import { StateSetter, DatasourceDataPanelProps, DatasourceMap } from '../../types'; @@ -19,17 +18,16 @@ import { switchDatasource, useLensDispatch, updateDatasourceState, - LensState, useLensSelector, setState, + selectExecutionContext, + selectActiveDatasourceId, + selectDatasourceStates, } from '../../state_management'; import { initializeDatasources } from './state_helpers'; interface DataPanelWrapperProps { - datasourceState: unknown; datasourceMap: DatasourceMap; - activeDatasource: string | null; - datasourceIsLoading: boolean; showNoDataPopover: () => void; core: DatasourceDataPanelProps['core']; dropOntoWorkspace: (field: DragDropIdentifier) => void; @@ -37,35 +35,27 @@ interface DataPanelWrapperProps { plugins: { uiActions: UiActionsStart }; } -const getExternals = createSelector( - (state: LensState) => state.lens, - ({ resolvedDateRange, query, filters, datasourceStates, activeDatasourceId }) => ({ - dateRange: resolvedDateRange, - query, - filters, - datasourceStates, - activeDatasourceId, - }) -); - export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { - const { activeDatasource } = props; + const externalContext = useLensSelector(selectExecutionContext); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + + const datasourceIsLoading = activeDatasourceId + ? datasourceStates[activeDatasourceId].isLoading + : true; - const { filters, query, dateRange, datasourceStates, activeDatasourceId } = useLensSelector( - getExternals - ); const dispatchLens = useLensDispatch(); const setDatasourceState: StateSetter = useMemo(() => { return (updater) => { dispatchLens( updateDatasourceState({ updater, - datasourceId: activeDatasource!, + datasourceId: activeDatasourceId!, clearStagedPreview: true, }) ); }; - }, [activeDatasource, dispatchLens]); + }, [activeDatasourceId, dispatchLens]); useEffect(() => { if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) { @@ -88,13 +78,11 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { }, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]); const datasourceProps: DatasourceDataPanelProps = { + ...externalContext, dragDropContext: useContext(DragContext), - state: props.datasourceState, + state: activeDatasourceId ? datasourceStates[activeDatasourceId].state : null, setState: setDatasourceState, core: props.core, - filters, - query, - dateRange, showNoDataPopover: props.showNoDataPopover, dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, @@ -135,7 +123,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { { setDatasourceSwitcher(false); dispatchLens(switchDatasource({ newDatasourceId: datasourceId })); @@ -147,10 +135,10 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { /> )} - {props.activeDatasource && !props.datasourceIsLoading && ( + {activeDatasourceId && !datasourceIsLoading && ( )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 4ce68dc3bc70..5d1652b8e1f7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React from 'react'; import { ReactWrapper } from 'enzyme'; // Tests are executed in a jsdom environment who does not have sizing methods, @@ -45,8 +45,7 @@ import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/pu import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { mockDataPlugin, mountWithProvider } from '../../mocks'; -import { setState, setToggleFullscreen } from '../../state_management'; -import { FrameLayout } from './frame_layout'; +import { setState } from '../../state_management'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -213,7 +212,7 @@ describe('editor_frame', () => { const props = { ...getDefaultProps(), visualizationMap: { - testVis: { ...mockVisualization, toExpression: () => 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }, datasourceMap: { testDatasource: { @@ -246,7 +245,7 @@ describe('editor_frame', () => { expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} - | vis" + | testVis" `); }); @@ -263,7 +262,7 @@ describe('editor_frame', () => { const props = { ...getDefaultProps(), visualizationMap: { - testVis: { ...mockVisualization, toExpression: () => 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }, datasourceMap: { testDatasource: { @@ -368,7 +367,7 @@ describe('editor_frame', () => { }, Object { "arguments": Object {}, - "function": "vis", + "function": "testVis", "type": "function", }, ], @@ -1100,45 +1099,5 @@ describe('editor_frame', () => { }) ); }); - - it('should avoid completely to compute suggestion when in fullscreen mode', async () => { - const props = { - ...getDefaultProps(), - initialContext: { - indexPatternId: '1', - fieldName: 'test', - }, - visualizationMap: { - testVis: mockVisualization, - }, - datasourceMap: { - testDatasource: mockDatasource, - testDatasource2: mockDatasource2, - }, - - ExpressionRenderer: expressionRendererMock, - }; - - const { instance: el, lensStore } = await mountWithProvider(, { - data: props.plugins.data, - }); - instance = el; - - expect( - instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement - ).not.toBeUndefined(); - - lensStore.dispatch(setToggleFullscreen()); - instance.update(); - - expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false); - - lensStore.dispatch(setToggleFullscreen()); - instance.update(); - - expect( - instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement - ).not.toBeUndefined(); - }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 653ad2d27fe0..0813d0deef02 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -5,21 +5,28 @@ * 2.0. */ -import React, { useCallback, useRef, useMemo } from 'react'; +import React, { useCallback, useRef } from 'react'; import { CoreStart } from 'kibana/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../types'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel'; import { FrameLayout } from './frame_layout'; -import { SuggestionPanel } from './suggestion_panel'; +import { SuggestionPanelWrapper } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop'; import { EditorFrameStartPlugins } from '../service'; -import { createDatasourceLayers } from './state_helpers'; import { getTopSuggestionForField, switchToSuggestion, Suggestion } from './suggestion_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { useLensSelector, useLensDispatch } from '../../state_management'; +import { + useLensSelector, + useLensDispatch, + selectAreDatasourcesLoaded, + selectFramePublicAPI, + selectActiveDatasourceId, + selectDatasourceStates, + selectVisualization, +} from '../../state_management'; export interface EditorFrameProps { datasourceMap: DatasourceMap; @@ -31,58 +38,27 @@ export interface EditorFrameProps { } export function EditorFrame(props: EditorFrameProps) { - const { - activeData, - resolvedDateRange: dateRange, - query, - filters, - searchSessionId, - activeDatasourceId, - visualization, - datasourceStates, - stagedPreview, - isFullscreenDatasource, - } = useLensSelector((state) => state.lens); - + const { datasourceMap, visualizationMap } = props; const dispatchLens = useLensDispatch(); - - const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); - - const datasourceLayers = React.useMemo( - () => createDatasourceLayers(props.datasourceMap, datasourceStates), - [props.datasourceMap, datasourceStates] - ); - - const framePublicAPI: FramePublicAPI = useMemo( - () => ({ - datasourceLayers, - activeData, - dateRange, - query, - filters, - searchSessionId, - }), - [activeData, datasourceLayers, dateRange, query, filters, searchSessionId] + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + const visualization = useLensSelector(selectVisualization); + const allLoaded = useLensSelector(selectAreDatasourcesLoaded); + const framePublicAPI: FramePublicAPI = useLensSelector((state) => + selectFramePublicAPI(state, datasourceMap) ); - // Using a ref to prevent rerenders in the child components while keeping the latest state const getSuggestionForField = useRef<(field: DragDropIdentifier) => Suggestion | undefined>(); getSuggestionForField.current = (field: DragDropIdentifier) => { - const activeVisualizationId = visualization.activeId; - const visualizationState = visualization.state; - const { visualizationMap, datasourceMap } = props; - if (!field || !activeDatasourceId) { return; } - return getTopSuggestionForField( - datasourceLayers, - activeVisualizationId, + framePublicAPI.datasourceLayers, + visualization, + datasourceStates, visualizationMap, - visualizationState, datasourceMap[activeDatasourceId], - datasourceStates, field ); }; @@ -106,18 +82,12 @@ export function EditorFrame(props: EditorFrameProps) { return ( @@ -125,50 +95,33 @@ export function EditorFrame(props: EditorFrameProps) { configPanel={ allLoaded && ( ) } workspacePanel={ allLoaded && ( ) } suggestionsPanel={ - allLoaded && - !isFullscreenDatasource && ( - ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 5d1f99cf0dec..4acc56f1dff3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -6,18 +6,13 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; +import { DatasourceStates } from '../../state_management'; import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, datasourceMap: DatasourceMap, - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - > + datasourceStates: DatasourceStates ): Ast | null { const datasourceExpressions: Array<[string, Ast | string]> = []; @@ -81,13 +76,7 @@ export function buildExpression({ visualization: Visualization | null; visualizationState: unknown; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; datasourceLayers: Record; }): Ast | null { if (visualization === null) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index f27e0f9c24d7..5175158e077f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -11,20 +11,22 @@ import React from 'react'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; +import { useLensSelector, selectIsFullscreenDatasource } from '../../state_management'; export interface FrameLayoutProps { dataPanel: React.ReactNode; configPanel?: React.ReactNode; suggestionsPanel?: React.ReactNode; workspacePanel?: React.ReactNode; - isFullscreen?: boolean; } export function FrameLayout(props: FrameLayoutProps) { + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); + return ( @@ -65,7 +67,7 @@ export function FrameLayout(props: FrameLayoutProps) {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 2b0090554b7d..5e059a1e2e8b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -28,15 +28,16 @@ import { getMissingIndexPatterns, getMissingVisualizationTypeError, } from '../error_helper'; +import { DatasourceStates } from '../../state_management'; export async function initializeDatasources( datasourceMap: DatasourceMap, - datasourceStates: Record, + datasourceStates: DatasourceStates, references?: SavedObjectReference[], initialContext?: VisualizeFieldContext, options?: InitializationOptions ) { - const states: Record = {}; + const states: DatasourceStates = {}; await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { if (datasourceStates[datasourceId]) { @@ -57,8 +58,8 @@ export async function initializeDatasources( } export const createDatasourceLayers = memoizeOne(function createDatasourceLayers( - datasourceMap: DatasourceMap, - datasourceStates: Record + datasourceStates: DatasourceStates, + datasourceMap: DatasourceMap ) { const datasourceLayers: Record = {}; Object.keys(datasourceMap) @@ -79,7 +80,7 @@ export const createDatasourceLayers = memoizeOne(function createDatasourceLayers }); export async function persistedStateToExpression( - datasources: Record, + datasourceMap: DatasourceMap, visualizations: VisualizationMap, doc: Document ): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> { @@ -98,7 +99,7 @@ export async function persistedStateToExpression( } const visualization = visualizations[visualizationType!]; const datasourceStates = await initializeDatasources( - datasources, + datasourceMap, Object.fromEntries( Object.entries(persistedDatasourceStates).map(([id, state]) => [ id, @@ -110,7 +111,7 @@ export async function persistedStateToExpression( { isFullEditor: false } ); - const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); + const datasourceLayers = createDatasourceLayers(datasourceStates, datasourceMap); const datasourceId = getActiveDatasourceIdFromDoc(doc); if (datasourceId == null) { @@ -121,7 +122,7 @@ export async function persistedStateToExpression( } const indexPatternValidation = validateRequiredIndexPatterns( - datasources[datasourceId], + datasourceMap[datasourceId], datasourceStates[datasourceId] ); @@ -133,7 +134,7 @@ export async function persistedStateToExpression( } const validationResult = validateDatasourceAndVisualization( - datasources[datasourceId], + datasourceMap[datasourceId], datasourceStates[datasourceId].state, visualization, visualizationState, @@ -146,7 +147,7 @@ export async function persistedStateToExpression( description, visualization, visualizationState, - datasourceMap: datasources, + datasourceMap, datasourceStates, datasourceLayers, }), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 6f33cc4b8aab..632989057b48 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -9,6 +9,7 @@ import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers'; import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks'; import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types'; import { PaletteOutput } from 'src/plugins/charts/public'; +import { DatasourceStates } from '../../state_management'; const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({ state, @@ -22,13 +23,7 @@ const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSu }); let datasourceMap: Record; -let datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } ->; +let datasourceStates: DatasourceStates; beforeEach(() => { datasourceMap = { @@ -525,13 +520,10 @@ describe('suggestion helpers', () => { getOperationForColumnId: jest.fn(), }, }, - 'vis1', + { activeId: 'vis1', state: {} }, + { mockindexpattern: { state: mockDatasourceState, isLoading: false } }, { vis1: mockVisualization1 }, - {}, datasourceMap.mock, - { - mockindexpattern: { state: mockDatasourceState, isLoading: false }, - }, { id: 'myfield', humanData: { label: 'myfieldLabel' } }, ]; }); @@ -558,7 +550,7 @@ describe('suggestion helpers', () => { it('should return nothing if datasource does not produce suggestions', () => { datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]); - defaultParams[2] = { + defaultParams[3] = { vis1: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; @@ -567,7 +559,7 @@ describe('suggestion helpers', () => { }); it('should not consider suggestion from other visualization if there is data', () => { - defaultParams[2] = { + defaultParams[3] = { vis1: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; @@ -593,7 +585,7 @@ describe('suggestion helpers', () => { previewIcon: 'empty', }, ]); - defaultParams[2] = { + defaultParams[3] = { vis1: mockVisualization1, vis2: mockVisualization2, vis3: mockVisualization3, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 31d54d26c304..65cd5ae35c6f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -22,7 +22,13 @@ import { VisualizationMap, } from '../../types'; import { DragDropIdentifier } from '../../drag_drop'; -import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management'; +import { + LensDispatch, + selectSuggestion, + switchVisualization, + DatasourceStates, + VisualizationState, +} from '../../state_management'; export interface Suggestion { visualizationId: string; @@ -60,13 +66,7 @@ export function getSuggestions({ mainPalette, }: { datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; activeVisualizationId: string | null; subVisualizationId?: string; @@ -143,13 +143,7 @@ export function getVisualizeFieldSuggestions({ visualizeTriggerFieldContext, }: { datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; activeVisualizationId: string | null; subVisualizationId?: string; @@ -228,11 +222,10 @@ export function switchToSuggestion( export function getTopSuggestionForField( datasourceLayers: Record, - activeVisualizationId: string | null, + visualization: VisualizationState, + datasourceStates: DatasourceStates, visualizationMap: Record>, - visualizationState: unknown, datasource: Datasource, - datasourceStates: Record, field: DragDropIdentifier ) { const hasData = Object.values(datasourceLayers).some( @@ -240,20 +233,20 @@ export function getTopSuggestionForField( ); const mainPalette = - activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette - ? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState) + visualization.activeId && visualizationMap[visualization.activeId]?.getMainPalette + ? visualizationMap[visualization.activeId].getMainPalette?.(visualization.state) : undefined; const suggestions = getSuggestions({ datasourceMap: { [datasource.id]: datasource }, datasourceStates, visualizationMap: - hasData && activeVisualizationId - ? { [activeVisualizationId]: visualizationMap[activeVisualizationId] } + hasData && visualization.activeId + ? { [visualization.activeId]: visualizationMap[visualization.activeId] } : visualizationMap, - activeVisualizationId, - visualizationState, + activeVisualizationId: visualization.activeId, + visualizationState: visualization.state, field, mainPalette, }); - return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; + return suggestions.find((s) => s.visualizationId === visualization.activeId) || suggestions[0]; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 44fb47001631..b63d2956cfe6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -16,12 +16,12 @@ import { } from '../../mocks'; import { act } from 'react-dom/test-utils'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; -import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; -import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; +import { SuggestionPanel, SuggestionPanelProps, SuggestionPanelWrapper } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { mountWithProvider } from '../../mocks'; +import { LensAppState, PreviewState, setState, setToggleFullscreen } from '../../state_management'; jest.mock('./suggestion_helpers'); @@ -38,6 +38,8 @@ describe('suggestion_panel', () => { let defaultProps: SuggestionPanelProps; + let preloadedState: Partial; + beforeEach(() => { mockVisualization = createMockVisualization(); mockDatasource = createMockDatasource('a'); @@ -49,7 +51,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', keptLayerIds: ['a'], }, @@ -58,36 +60,58 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', keptLayerIds: ['a'], }, ] as Suggestion[]); - defaultProps = { - activeDatasourceId: 'mock', - datasourceMap: { - mock: mockDatasource, - }, + preloadedState = { datasourceStates: { - mock: { + testDatasource: { isLoading: false, - state: {}, + state: '', }, }, - activeVisualizationId: 'vis', + visualization: { + activeId: 'testVis', + state: {}, + }, + activeDatasourceId: 'testDatasource', + }; + + defaultProps = { + datasourceMap: { + testDatasource: mockDatasource, + }, visualizationMap: { - vis: mockVisualization, + testVis: mockVisualization, vis2: createMockVisualization(), }, - visualizationState: {}, ExpressionRenderer: expressionRendererMock, frame: createMockFramePublicAPI(), }; }); + it('should avoid completely to render SuggestionPanel when in fullscreen mode', async () => { + const { instance, lensStore } = await mountWithProvider( + + ); + expect(instance.find(SuggestionPanel).exists()).toBe(true); + + lensStore.dispatch(setToggleFullscreen()); + instance.update(); + expect(instance.find(SuggestionPanel).exists()).toBe(false); + + lensStore.dispatch(setToggleFullscreen()); + instance.update(); + expect(instance.find(SuggestionPanel).exists()).toBe(true); + }); + it('should list passed in suggestions', async () => { - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState, + }); expect( instance @@ -98,62 +122,56 @@ describe('suggestion_panel', () => { }); describe('uncommitted suggestions', () => { - let suggestionState: Pick< - SuggestionPanelProps, - 'datasourceStates' | 'activeVisualizationId' | 'visualizationState' - >; - let stagedPreview: SuggestionPanelProps['stagedPreview']; + let suggestionState: Pick; + let stagedPreview: PreviewState; beforeEach(() => { suggestionState = { datasourceStates: { - mock: { + testDatasource: { isLoading: false, - state: {}, + state: '', }, }, - activeVisualizationId: 'vis2', - visualizationState: {}, + visualization: { + activeId: 'vis2', + state: {}, + }, }; stagedPreview = { - datasourceStates: defaultProps.datasourceStates, - visualization: { - state: defaultProps.visualizationState, - activeId: defaultProps.activeVisualizationId, - }, + datasourceStates: preloadedState.datasourceStates!, + visualization: preloadedState.visualization!, }; }); it('should not update suggestions if current state is moved to staged preview', async () => { - const { instance } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider( + , + { preloadedState } + ); getSuggestionsMock.mockClear(); - instance.setProps({ - stagedPreview, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview })); instance.update(); expect(getSuggestionsMock).not.toHaveBeenCalled(); }); it('should update suggestions if staged preview is removed', async () => { - const { instance } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider( + , + { preloadedState } + ); getSuggestionsMock.mockClear(); - instance.setProps({ - stagedPreview, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview, ...suggestionState })); instance.update(); - instance.setProps({ - stagedPreview: undefined, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview: undefined, ...suggestionState })); instance.update(); expect(getSuggestionsMock).toHaveBeenCalledTimes(1); }); it('should highlight currently active suggestion', async () => { - const { instance } = await mountWithProvider(); - + const { instance } = await mountWithProvider(, { + preloadedState, + }); act(() => { instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); }); @@ -189,12 +207,14 @@ describe('suggestion_panel', () => { }); it('should dispatch visualization switch action if suggestion is clicked', async () => { - const { instance, lensStore } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider(, { + preloadedState, + }); act(() => { instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click'); }); - instance.update(); + // instance.update(); expect(lensStore.dispatch).toHaveBeenCalledWith( expect.objectContaining({ @@ -203,7 +223,7 @@ describe('suggestion_panel', () => { datasourceId: undefined, datasourceState: {}, initialState: { suggestion1: true }, - newVisualizationId: 'vis', + newVisualizationId: 'testVis', }, }) ); @@ -217,7 +237,7 @@ describe('suggestion_panel', () => { previewIcon: LensIconChartDatatable, score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', }, { @@ -225,7 +245,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', previewExpression: 'test | expression', }, @@ -239,7 +259,9 @@ describe('suggestion_panel', () => { mockDatasource.toExpression.mockReturnValue('datasource_expression'); - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState, + }); expect(instance.find(EuiIcon)).toHaveLength(1); expect(instance.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable); @@ -252,17 +274,20 @@ describe('suggestion_panel', () => { indexPatterns: {}, }; mockDatasource.checkIntegrity.mockReturnValue(['a']); - const newProps = { - ...defaultProps, + + const newPreloadedState = { + ...preloadedState, datasourceStates: { - mock: { - ...defaultProps.datasourceStates.mock, + testDatasource: { + ...preloadedState.datasourceStates!.testDatasource, state: missingIndexPatternsState, }, }, }; - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState: newPreloadedState, + }); expect(instance.html()).toEqual(null); }); @@ -274,7 +299,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', }, { @@ -282,7 +307,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', }, ] as Suggestion[]); @@ -291,18 +316,7 @@ describe('suggestion_panel', () => { (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression'); mockDatasource.toExpression.mockReturnValue('datasource_expression'); - const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - - mountWithProvider( - - ); + mountWithProvider(); expect(expressionRendererMock).toHaveBeenCalledTimes(1); const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index c6dae5b23b9d..858fcedf215e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -42,30 +42,28 @@ import { prependDatasourceExpression } from './expression_helpers'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers'; import { - PreviewState, rollbackSuggestion, + selectExecutionContextSearch, submitSuggestion, useLensDispatch, + useLensSelector, + selectCurrentVisualization, + selectCurrentDatasourceStates, + DatasourceStates, + selectIsFullscreenDatasource, + selectSearchSessionId, + selectActiveDatasourceId, + selectActiveData, + selectDatasourceStates, } from '../../state_management'; const MAX_SUGGESTIONS_DISPLAYED = 5; export interface SuggestionPanelProps { - activeDatasourceId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - activeVisualizationId: string | null; visualizationMap: VisualizationMap; - visualizationState: unknown; ExpressionRenderer: ReactExpressionRendererType; frame: FramePublicAPI; - stagedPreview?: PreviewState; } const PreviewRenderer = ({ @@ -173,128 +171,114 @@ const SuggestionPreview = ({ ); }; +export const SuggestionPanelWrapper = (props: SuggestionPanelProps) => { + const isFullscreenDatasource = useLensSelector(selectIsFullscreenDatasource); + return isFullscreenDatasource ? null : ; +}; + export function SuggestionPanel({ - activeDatasourceId, datasourceMap, - datasourceStates, - activeVisualizationId, visualizationMap, - visualizationState, frame, ExpressionRenderer: ExpressionRendererComponent, - stagedPreview, }: SuggestionPanelProps) { const dispatchLens = useLensDispatch(); - - const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates; - const currentVisualizationState = stagedPreview - ? stagedPreview.visualization.state - : visualizationState; - const currentVisualizationId = stagedPreview - ? stagedPreview.visualization.activeId - : activeVisualizationId; + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const activeData = useLensSelector(selectActiveData); + const datasourceStates = useLensSelector(selectDatasourceStates); + const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview)); + const currentVisualization = useLensSelector(selectCurrentVisualization); + const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates); const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, activeDatasourceId ? datasourceStates[activeDatasourceId] : null ); - const { suggestions, currentStateExpression, currentStateError } = useMemo( - () => { - const newSuggestions = missingIndexPatterns.length - ? [] - : getSuggestions({ - datasourceMap, - datasourceStates: currentDatasourceStates, - visualizationMap, - activeVisualizationId: currentVisualizationId, - visualizationState: currentVisualizationState, - activeData: frame.activeData, - }) - .filter( - ({ - hide, - visualizationId, - visualizationState: suggestionVisualizationState, - datasourceState: suggestionDatasourceState, - datasourceId: suggetionDatasourceId, - }) => { - return ( - !hide && - validateDatasourceAndVisualization( - suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, - suggestionDatasourceState, - visualizationMap[visualizationId], - suggestionVisualizationState, - frame - ) == null - ); - } - ) - .slice(0, MAX_SUGGESTIONS_DISPLAYED) - .map((suggestion) => ({ - ...suggestion, - previewExpression: preparePreviewExpression( - suggestion, - visualizationMap[suggestion.visualizationId], - datasourceMap, - currentDatasourceStates, - frame - ), - })); - - const validationErrors = validateDatasourceAndVisualization( - activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, - currentVisualizationId ? visualizationMap[currentVisualizationId] : null, - currentVisualizationState, - frame - ); - - const newStateExpression = - currentVisualizationState && currentVisualizationId && !validationErrors - ? preparePreviewExpression( - { visualizationState: currentVisualizationState }, - visualizationMap[currentVisualizationId], + const { suggestions, currentStateExpression, currentStateError } = useMemo(() => { + const newSuggestions = missingIndexPatterns.length + ? [] + : getSuggestions({ + datasourceMap, + datasourceStates: currentDatasourceStates, + visualizationMap, + activeVisualizationId: currentVisualization.activeId, + visualizationState: currentVisualization.state, + activeData, + }) + .filter( + ({ + hide, + visualizationId, + visualizationState: suggestionVisualizationState, + datasourceState: suggestionDatasourceState, + datasourceId: suggetionDatasourceId, + }) => { + return ( + !hide && + validateDatasourceAndVisualization( + suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, + suggestionDatasourceState, + visualizationMap[visualizationId], + suggestionVisualizationState, + frame + ) == null + ); + } + ) + .slice(0, MAX_SUGGESTIONS_DISPLAYED) + .map((suggestion) => ({ + ...suggestion, + previewExpression: preparePreviewExpression( + suggestion, + visualizationMap[suggestion.visualizationId], datasourceMap, currentDatasourceStates, frame - ) - : undefined; - - return { - suggestions: newSuggestions, - currentStateExpression: newStateExpression, - currentStateError: validationErrors, - }; - }, + ), + })); + + const validationErrors = validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, + currentVisualization.activeId ? visualizationMap[currentVisualization.activeId] : null, + currentVisualization.state, + frame + ); + + const newStateExpression = + currentVisualization.state && currentVisualization.activeId && !validationErrors + ? preparePreviewExpression( + { visualizationState: currentVisualization.state }, + visualizationMap[currentVisualization.activeId], + datasourceMap, + currentDatasourceStates, + frame + ) + : undefined; + + return { + suggestions: newSuggestions, + currentStateExpression: newStateExpression, + currentStateError: validationErrors, + }; // eslint-disable-next-line react-hooks/exhaustive-deps - [ - currentDatasourceStates, - currentVisualizationState, - currentVisualizationId, - activeDatasourceId, - datasourceMap, - visualizationMap, - ] - ); + }, [ + currentDatasourceStates, + currentVisualization.state, + currentVisualization.activeId, + activeDatasourceId, + datasourceMap, + visualizationMap, + ]); - const context: ExecutionContextSearch = useMemo( - () => ({ - query: frame.query, - timeRange: { - from: frame.dateRange.fromDate, - to: frame.dateRange.toDate, - }, - filters: frame.filters, - }), - [frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters] - ); + const context: ExecutionContextSearch = useLensSelector(selectExecutionContextSearch); + const searchSessionId = useLensSelector(selectSearchSessionId); const contextRef = useRef(context); contextRef.current = context; - const sessionIdRef = useRef(frame.searchSessionId); - sessionIdRef.current = frame.searchSessionId; + const sessionIdRef = useRef(searchSessionId); + sessionIdRef.current = searchSessionId; const AutoRefreshExpressionRenderer = useMemo(() => { return (props: ReactExpressionRendererProps) => ( @@ -312,11 +296,11 @@ export function SuggestionPanel({ // if the staged preview is overwritten by a suggestion, // reset the selected index to "current visualization" because // we are not in transient suggestion state anymore - if (!stagedPreview && lastSelectedSuggestion !== -1) { + if (!existsStagedPreview && lastSelectedSuggestion !== -1) { setLastSelectedSuggestion(-1); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stagedPreview]); + }, [existsStagedPreview]); if (!activeDatasourceId) { return null; @@ -347,7 +331,7 @@ export function SuggestionPanel({

- {stagedPreview && ( + {existsStagedPreview && (
- {currentVisualizationId && ( + {currentVisualization.activeId && ( >, - datasourceStates: Record, + datasourceMap: DatasourceMap, + datasourceStates: DatasourceStates, framePublicAPI: FramePublicAPI ) { const suggestionDatasourceId = visualizableState.datasourceId; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 9b5766c3e3bf..9e80dcfc4742 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -11,6 +11,7 @@ import { createMockVisualization, createMockFramePublicAPI, createMockDatasource, + mockDatasourceStates, } from '../../../mocks'; import { mountWithProvider } from '../../../mocks'; @@ -163,15 +164,6 @@ describe('chart_switch', () => { }; } - function mockDatasourceStates() { - return { - testDatasource: { - state: {}, - isLoading: false, - }, - }; - } - function showFlyout(instance: ReactWrapper) { instance.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click'); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 7db639536b8a..e2036e556a55 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -35,6 +35,11 @@ import { updateVisualizationState, useLensDispatch, useLensSelector, + VisualizationState, + DatasourceStates, + selectActiveDatasourceId, + selectVisualization, + selectDatasourceStates, } from '../../../state_management'; import { generateId } from '../../../id_generator/id_generator'; @@ -111,9 +116,9 @@ function getCurrentVisualizationId( export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const dispatchLens = useLensDispatch(); - const activeDatasourceId = useLensSelector((state) => state.lens.activeDatasourceId); - const visualization = useLensSelector((state) => state.lens.visualization); - const datasourceStates = useLensSelector((state) => state.lens.datasourceStates); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const visualization = useLensSelector(selectVisualization); + const datasourceStates = useLensSelector(selectDatasourceStates); function removeLayers(layerIds: string[]) { const activeVisualization = @@ -498,11 +503,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - datasourceStates: Record, - visualization: { - activeId: string | null; - state: unknown; - }, + datasourceStates: DatasourceStates, + visualization: VisualizationState, newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 784455cc9f6d..e687c3ea4442 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -33,6 +33,7 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; +import { LensRootStore, setState } from '../../../state_management'; const defaultPermissions: Record>> = { navLinks: { management: true }, @@ -49,12 +50,8 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { } const defaultProps = { - activeDatasourceId: 'mock', - datasourceStates: {}, datasourceMap: {}, framePublicAPI: createMockFramePublicAPI(), - activeVisualizationId: 'vis', - visualizationState: {}, ExpressionRenderer: createExpressionRendererMock(), core: createCoreStartWithPermissions(), plugins: { @@ -62,7 +59,6 @@ const defaultProps = { data: mockDataPlugin(), }, getSuggestionForField: () => undefined, - isFullscreen: false, toggleFullscreen: jest.fn(), }; @@ -84,7 +80,7 @@ describe('workspace_panel', () => { uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource('a'); + mockDatasource = createMockDatasource('testDatasource'); expressionRendererMock = createExpressionRendererMock(); }); @@ -96,14 +92,16 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} }, + } ); instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); @@ -115,11 +113,11 @@ describe('workspace_panel', () => { null }, + testVis: { ...mockVisualization, toExpression: () => null }, }} />, - { data: defaultProps.plugins.data } + { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -132,11 +130,11 @@ describe('workspace_panel', () => { 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, - { data: defaultProps.plugins.data } + { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -155,18 +153,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -178,7 +170,7 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} - | vis" + | testVis" `); }); @@ -194,18 +186,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} plugins={{ ...props.plugins, uiActions: uiActionsMock }} @@ -235,18 +221,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -283,28 +263,32 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, + mock2: { + state: {}, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -364,18 +348,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -418,18 +396,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -470,23 +442,27 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -504,20 +480,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} // Use cannot navigate to the management page core={createCoreStartWithPermissions({ @@ -526,7 +494,18 @@ describe('workspace_panel', () => { })} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -545,19 +524,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} // user can go to management, but indexPatterns management is not accessible core={createCoreStartWithPermissions({ @@ -566,7 +538,18 @@ describe('workspace_panel', () => { })} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -588,18 +571,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, @@ -617,7 +594,7 @@ describe('workspace_panel', () => { mockVisualization.getErrorMessages.mockReturnValue([ { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, ]); - mockVisualization.toExpression.mockReturnValue('vis'); + mockVisualization.toExpression.mockReturnValue('testVis'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -626,18 +603,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , @@ -657,7 +628,7 @@ describe('workspace_panel', () => { mockVisualization.getErrorMessages.mockReturnValue([ { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, ]); - mockVisualization.toExpression.mockReturnValue('vis'); + mockVisualization.toExpression.mockReturnValue('testVis'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -666,18 +637,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , @@ -703,18 +668,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, @@ -738,18 +697,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -775,23 +728,17 @@ describe('workspace_panel', () => { framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - + let lensStore: LensRootStore; await act(async () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -799,6 +746,7 @@ describe('workspace_panel', () => { { data: defaultProps.plugins.data } ); instance = mounted.instance; + lensStore = mounted.lensStore; }); instance.update(); @@ -809,7 +757,14 @@ describe('workspace_panel', () => { return ; }); - instance.setProps({ visualizationState: {} }); + lensStore!.dispatch( + setState({ + visualization: { + activeId: 'testVis', + state: {}, + }, + }) + ); instance.update(); expect(expressionRendererMock).toHaveBeenCalledTimes(2); @@ -843,18 +798,12 @@ describe('workspace_panel', () => { > { it('should immediately transition if exactly one suggestion is returned', async () => { mockGetSuggestionForField.mockReturnValue({ - visualizationId: 'vis', + visualizationId: 'testVis', datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', visualizationState: {}, }); const { lensStore } = await initComponent(); @@ -879,19 +828,19 @@ describe('workspace_panel', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - newVisualizationId: 'vis', + newVisualizationId: 'testVis', initialState: {}, datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', }, }); }); it('should allow to drop if there are suggestions', async () => { mockGetSuggestionForField.mockReturnValue({ - visualizationId: 'vis', + visualizationId: 'testVis', datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', visualizationState: {}, }); await initComponent(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 68243efc28d0..8b49c72b3cff 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -40,6 +40,7 @@ import { isLensEditEvent, VisualizationMap, DatasourceMap, + DatasourceFixAction, } from '../../../types'; import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop'; import { Suggestion, switchToSuggestion } from '../suggestion_helpers'; @@ -58,34 +59,30 @@ import { updateVisualizationState, updateDatasourceState, setSaveable, + useLensSelector, + selectExecutionContext, + selectIsFullscreenDatasource, + selectVisualization, + selectDatasourceStates, + selectActiveDatasourceId, + selectSearchSessionId, } from '../../../state_management'; export interface WorkspacePanelProps { - activeVisualizationId: string | null; visualizationMap: VisualizationMap; - visualizationState: unknown; - activeDatasourceId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - state: unknown; - isLoading: boolean; - } - >; framePublicAPI: FramePublicAPI; ExpressionRenderer: ReactExpressionRendererType; core: CoreStart; plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined; - isFullscreen: boolean; } interface WorkspaceState { expressionBuildError?: Array<{ shortMessage: string; longMessage: string; - fixAction?: { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise }; + fixAction?: DatasourceFixAction; }>; expandError: boolean; } @@ -120,29 +117,30 @@ export const WorkspacePanel = React.memo(function WorkspacePanel(props: Workspac // Exported for testing purposes only. export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ - activeDatasourceId, - activeVisualizationId, + framePublicAPI, visualizationMap, - visualizationState, datasourceMap, - datasourceStates, - framePublicAPI, core, plugins, ExpressionRenderer: ExpressionRendererComponent, suggestionForDraggedField, - isFullscreen, }: Omit & { suggestionForDraggedField: Suggestion | undefined; }) { const dispatchLens = useLensDispatch(); + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); + const visualization = useLensSelector(selectVisualization); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + + const { datasourceLayers } = framePublicAPI; const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, }); - const activeVisualization = activeVisualizationId - ? visualizationMap[activeVisualizationId] + const activeVisualization = visualization.activeId + ? visualizationMap[visualization.activeId] : null; const missingIndexPatterns = getMissingIndexPattern( @@ -175,64 +173,60 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId ? datasourceMap[activeDatasourceId] : null, activeDatasourceId && datasourceStates[activeDatasourceId]?.state, activeVisualization, - visualizationState, + visualization.state, framePublicAPI ), // eslint-disable-next-line react-hooks/exhaustive-deps - [activeVisualization, visualizationState, activeDatasourceId, datasourceMap, datasourceStates] + [activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates] ); - const expression = useMemo( - () => { - if (!configurationValidationError?.length && !missingRefsErrors.length) { - try { - const ast = buildExpression({ - visualization: activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - datasourceLayers: framePublicAPI.datasourceLayers, - }); - - if (ast) { - // expression has to be turned into a string for dirty checking - if the ast is rebuilt, - // turning it into a string will make sure the expression renderer only re-renders if the - // expression actually changed. - return toExpression(ast); - } else { - return null; - } - } catch (e) { - const buildMessages = activeVisualization?.getErrorMessages(visualizationState); - const defaultMessage = { - shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { - defaultMessage: 'An unexpected error occurred while preparing the chart', - }), - longMessage: e.toString(), - }; - // Most likely an error in the expression provided by a datasource or visualization - setLocalState((s) => ({ - ...s, - expressionBuildError: buildMessages ?? [defaultMessage], - })); + const expression = useMemo(() => { + if (!configurationValidationError?.length && !missingRefsErrors.length) { + try { + const ast = buildExpression({ + visualization: activeVisualization, + visualizationState: visualization.state, + datasourceMap, + datasourceStates, + datasourceLayers, + }); + + if (ast) { + // expression has to be turned into a string for dirty checking - if the ast is rebuilt, + // turning it into a string will make sure the expression renderer only re-renders if the + // expression actually changed. + return toExpression(ast); + } else { + return null; } + } catch (e) { + const buildMessages = activeVisualization?.getErrorMessages(visualization.state); + const defaultMessage = { + shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { + defaultMessage: 'An unexpected error occurred while preparing the chart', + }), + longMessage: e.toString(), + }; + // Most likely an error in the expression provided by a datasource or visualization + setLocalState((s) => ({ + ...s, + expressionBuildError: buildMessages ?? [defaultMessage], + })); } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - framePublicAPI.dateRange, - framePublicAPI.query, - framePublicAPI.filters, - ] - ); + } + }, [ + activeVisualization, + visualization.state, + datasourceMap, + datasourceStates, + datasourceLayers, + configurationValidationError?.length, + missingRefsErrors.length, + ]); const expressionExists = Boolean(expression); const hasLoaded = Boolean( - activeVisualization && visualizationState && datasourceMap && datasourceStates + activeVisualization && visualization.state && datasourceMap && datasourceStates ); useEffect(() => { if (hasLoaded) { @@ -392,8 +386,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ return ( Promise }; + fixAction?: DatasourceFixAction; }>; missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>; }; @@ -432,22 +426,19 @@ export const VisualizationWrapper = ({ application: ApplicationStart; activeDatasourceId: string | null; }) => { - const context: ExecutionContextSearch = useMemo( + const context = useLensSelector(selectExecutionContext); + const searchContext: ExecutionContextSearch = useMemo( () => ({ - query: framePublicAPI.query, + query: context.query, timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, + from: context.dateRange.fromDate, + to: context.dateRange.toDate, }, - filters: framePublicAPI.filters, + filters: context.filters, }), - [ - framePublicAPI.query, - framePublicAPI.dateRange.fromDate, - framePublicAPI.dateRange.toDate, - framePublicAPI.filters, - ] + [context] ); + const searchSessionId = useLensSelector(selectSearchSessionId); const dispatchLens = useLensDispatch(); @@ -465,9 +456,7 @@ export const VisualizationWrapper = ({ | { shortMessage: string; longMessage: string; - fixAction?: - | { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise } - | undefined; + fixAction?: DatasourceFixAction; } | undefined ) { @@ -480,7 +469,10 @@ export const VisualizationWrapper = ({ data-test-subj="errorFixAction" onClick={async () => { trackUiEvent('error_fix_action'); - const newState = await validationError.fixAction?.newState(framePublicAPI); + const newState = await validationError.fixAction?.newState({ + ...framePublicAPI, + ...context, + }); dispatchLens( updateDatasourceState({ updater: newState, @@ -638,8 +630,8 @@ export const VisualizationWrapper = ({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} - searchContext={context} - searchSessionId={framePublicAPI.searchSessionId} + searchContext={searchContext} + searchSessionId={searchSessionId} onEvent={onEvent} onData$={onData$} renderMode="edit" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 057d8c8baebf..e1cb1aeb9f82 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -14,23 +14,22 @@ import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types' import { NativeRenderer } from '../../../native_renderer'; import { ChartSwitch } from './chart_switch'; import { WarningsPopover } from './warnings_popover'; -import { useLensDispatch, updateVisualizationState } from '../../../state_management'; +import { + useLensDispatch, + updateVisualizationState, + DatasourceStates, + VisualizationState, +} from '../../../state_management'; import { WorkspaceTitle } from './title'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; framePublicAPI: FramePublicAPI; - visualizationState: unknown; + visualizationState: VisualizationState['state']; visualizationMap: VisualizationMap; visualizationId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; isFullscreen: boolean; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 5cf2abf7b8d0..56abf499aac8 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -128,7 +128,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { timeRange: { @@ -168,7 +167,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { timeRange: { @@ -212,7 +210,6 @@ describe('embeddable', () => { }, errors: [{ shortMessage: '', longMessage: 'my validation error' }], }), - executionContext: coreMock.createStart().executionContext, }, {} as LensEmbeddableInput ); @@ -256,7 +253,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, {} as LensEmbeddableInput ); @@ -295,7 +291,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -337,7 +332,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -386,7 +380,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -433,7 +426,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -487,7 +479,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -541,7 +532,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -594,7 +584,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -636,7 +625,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -678,7 +666,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -720,7 +707,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123', timeRange, query, filters } as LensEmbeddableInput ); @@ -777,7 +763,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onLoad } as unknown) as LensEmbeddableInput ); @@ -850,7 +835,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onFilter } as unknown) as LensEmbeddableInput ); @@ -898,7 +882,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onBrushEnd } as unknown) as LensEmbeddableInput ); @@ -946,7 +929,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onTableRowClick } as unknown) as LensEmbeddableInput ); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index e26466be6f81..4ecb86ac1069 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -8,7 +8,6 @@ import { isEqual, uniqBy } from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import type { ExecutionContextServiceStart } from 'src/core/public'; import { ExecutionContextSearch, Filter, @@ -98,7 +97,6 @@ export interface LensEmbeddableDeps { getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; usageCollection?: UsageCollectionSetup; - executionContext: ExecutionContextServiceStart; } export class Embeddable @@ -324,13 +322,16 @@ export class Embeddable if (this.input.onLoad) { this.input.onLoad(true); } - const executionContext = this.deps.executionContext.create({ + + const executionContext = { type: 'lens', name: this.savedVis.visualizationType ?? '', - description: this.savedVis.title ?? this.savedVis.description ?? '', id: this.id, + description: this.savedVis.title || this.input.title || '', url: this.output.editUrl, - }); + parent: this.input.executionContext, + }; + const input = this.getInput(); render( diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index ba4e4df84948..4cc074b5e830 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - Capabilities, - HttpSetup, - SavedObjectReference, - ExecutionContextServiceStart, -} from 'kibana/public'; +import type { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; @@ -38,7 +33,6 @@ export interface LensEmbeddableStartServices { indexPatternService: IndexPatternsContract; uiActions?: UiActionsStart; usageCollection?: UsageCollectionSetup; - executionContext: ExecutionContextServiceStart; documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; @@ -93,7 +87,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, capabilities, usageCollection, - executionContext, } = await this.getStartServices(); const { Embeddable } = await import('../async_services'); @@ -113,7 +106,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { canSaveVisualizations: Boolean(capabilities.visualize.save), }, usageCollection, - executionContext, }, input, parent diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index b61dba162302..fc6fcee9428b 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -14,7 +14,7 @@ import { ReactExpressionRendererType, ReactExpressionRendererProps, } from 'src/plugins/expressions/public'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import classNames from 'classnames'; @@ -40,7 +40,7 @@ export interface ExpressionWrapperProps { className?: string; canEdit: boolean; onRuntimeError: () => void; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } interface VisualizationErrorProps { diff --git a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx index 27be4b9ce7fe..98ce4b399ae8 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx @@ -20,7 +20,7 @@ import type { HeatmapExpressionProps } from './types'; export { heatmapGridConfig, heatmapLegendConfig, heatmap } from '../../common/expressions'; export const getHeatmapRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; timeZone: string; @@ -37,7 +37,6 @@ export const getHeatmapRenderer = (dependencies: { config: HeatmapExpressionProps, handlers: IInterpreterRenderHandlers ) => { - const formatFactory = await dependencies.formatFactory; const onClickValue = (data: LensFilterEvent['data']) => { handlers.event({ name: 'filter', data }); }; @@ -53,7 +52,7 @@ export const getHeatmapRenderer = (dependencies: { onClickValue={onClickValue} onSelectRange={onSelectRange} timeZone={dependencies.timeZone} - formatFactory={formatFactory} + formatFactory={dependencies.formatFactory} chartsThemeService={dependencies.chartsThemeService} paletteService={dependencies.paletteService} /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/index.ts b/x-pack/plugins/lens/public/heatmap_visualization/index.ts index 11f9b907eb92..5fb4524939f1 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/index.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/index.ts @@ -14,7 +14,7 @@ import type { FormatFactory } from '../../common'; export interface HeatmapVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 03eb234d9076..59aebc517bf2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -20,6 +20,7 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/index_pattern_field_editor/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; @@ -251,6 +252,7 @@ describe('IndexPattern Data Panel', () => { indexPatternRefs: [], existingFields: {}, data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), onUpdateIndexPattern: jest.fn(), dragDropContext: createMockedDragDropContext(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 3f359f214fe4..5db5404f3d9d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -23,14 +23,15 @@ import { EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EsQueryConfig, Query, Filter } from '@kbn/es-query'; +import type { EsQueryConfig, Query, Filter } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { CoreStart } from 'kibana/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { htmlIdGenerator } from '@elastic/eui'; -import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; +import type { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; -import { +import type { IndexPattern, IndexPatternPrivateState, IndexPatternField, @@ -46,6 +47,7 @@ import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../../src/plugins/ui_actio export type Props = Omit, 'core'> & { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; changeIndexPattern: ( id: string, state: IndexPatternPrivateState, @@ -118,6 +120,7 @@ export function IndexPatternDataPanel({ dragDropContext, core, data, + fieldFormats, query, filters, dateRange, @@ -231,6 +234,7 @@ export function IndexPatternDataPanel({ dragDropContext={dragDropContext} core={core} data={data} + fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={indexPatternFieldEditor} onChangeIndexPattern={onChangeIndexPattern} @@ -289,6 +293,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ onUpdateIndexPattern, core, data, + fieldFormats, indexPatternFieldEditor, existingFields, charts, @@ -297,6 +302,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ uiActions, }: Omit & { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; @@ -347,23 +353,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ supportedFieldTypes.has(field.type) ); const sorted = allSupportedTypesFields.sort(sortFields); - let groupedFields; - // optimization before existingFields are synced - if (!hasSyncedExistingFields) { - groupedFields = { - ...defaultFieldGroups, - ...groupBy(sorted, (field) => { - if (field.type === 'document') { - return 'specialFields'; - } else if (field.meta) { - return 'metaFields'; - } else { - return 'emptyFields'; - } - }), - }; - } - groupedFields = { + const groupedFields = { ...defaultFieldGroups, ...groupBy(sorted, (field) => { if (field.type === 'document') { @@ -455,7 +445,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ return fieldGroupDefinitions; }, [ allFields, - hasSyncedExistingFields, fieldInfoUnavailable, filters.length, existenceFetchTimeout, @@ -582,6 +571,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ () => ({ core, data, + fieldFormats, indexPattern: currentIndexPattern, highlight: localState.nameFilter.toLowerCase(), dateRange, @@ -592,6 +582,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ [ core, data, + fieldFormats, currentIndexPattern, dateRange, query, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 531825579264..b6d3a230d06f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,7 +7,7 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import 'jest-canvas-mock'; -import React, { ChangeEvent, MouseEvent, ReactElement } from 'react'; +import React, { ChangeEvent, MouseEvent } from 'react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox, @@ -16,7 +16,6 @@ import { EuiRange, EuiSelect, EuiButtonIcon, - EuiPopover, } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { @@ -33,7 +32,7 @@ import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; -import { Filtering } from './filtering'; +import { Filtering, setFilter } from './filtering'; import { TimeShift } from './time_shift'; import { DimensionEditor } from './dimension_editor'; import { AdvancedOptions } from './advanced_options'; @@ -1541,9 +1540,13 @@ describe('IndexPatternDimensionEditorPanel', () => { {...getProps({ filter: { language: 'kuery', query: 'a: b' } })} /> ); + expect( - (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.value - ).toEqual({ language: 'kuery', query: 'a: b' }); + wrapper + .find(Filtering) + .find('button[data-test-subj="indexPattern-filters-existingFilterTrigger"]') + .text() + ).toBe(`a: b`); }); it('should allow to set filter initially', () => { @@ -1609,11 +1612,15 @@ describe('IndexPatternDimensionEditorPanel', () => { const props = getProps({ filter: { language: 'kuery', query: 'a: b' }, }); + wrapper = mount(); - (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.onChange({ - language: 'kuery', - query: 'c: d', + + act(() => { + const { updateLayer, columnId, layer } = wrapper.find(Filtering).props(); + + updateLayer(setFilter(columnId, layer, { language: 'kuery', query: 'c: d' })); }); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx index 68705ebf2d15..ddd971883964 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx @@ -4,16 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiButtonIcon, EuiLink, EuiPanel, EuiPopover } from '@elastic/eui'; -import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { Query } from 'src/plugins/data/public'; +import { isEqual } from 'lodash'; +import { + EuiButtonIcon, + EuiLink, + EuiPanel, + EuiPopover, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiPopoverProps, +} from '@elastic/eui'; +import type { Query } from 'src/plugins/data/public'; import { IndexPatternColumn, operationDefinitionMap } from '../operations'; -import { isQueryValid } from '../operations/definitions/filters'; +import { validateQuery } from '../operations/definitions/filters'; import { QueryInput } from '../query_input'; -import { IndexPattern, IndexPatternLayer } from '../types'; +import type { IndexPattern, IndexPatternLayer } from '../types'; + +const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', { + defaultMessage: 'Filter by', +}); // to do: get the language from uiSettings export const defaultFilter: Query = { @@ -49,29 +61,49 @@ export function Filtering({ updateLayer: (newLayer: IndexPatternLayer) => void; isInitiallyOpen: boolean; }) { + const inputFilter = selectedColumn.filter; + const [queryInput, setQueryInput] = useState(inputFilter ?? defaultFilter); const [filterPopoverOpen, setFilterPopoverOpen] = useState(isInitiallyOpen); + + useEffect(() => { + const { isValid } = validateQuery(queryInput, indexPattern); + + if (isValid && !isEqual(inputFilter, queryInput)) { + updateLayer(setFilter(columnId, layer, queryInput)); + } + }, [columnId, layer, queryInput, indexPattern, updateLayer, inputFilter]); + + const onClosePopup: EuiPopoverProps['closePopover'] = useCallback(() => { + setFilterPopoverOpen(false); + if (inputFilter) { + setQueryInput(inputFilter); + } + }, [inputFilter]); + const selectedOperation = operationDefinitionMap[selectedColumn.operationType]; - if (!selectedOperation.filterable || !selectedColumn.filter) { + + if (!selectedOperation.filterable || !inputFilter) { return null; } - const isInvalid = !isQueryValid(selectedColumn.filter, indexPattern); + const { isValid: isInputFilterValid } = validateQuery(inputFilter, indexPattern); + const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( + queryInput, + indexPattern + ); return ( { - setFilterPopoverOpen(false); - }} + closePopover={onClosePopup} anchorClassName="eui-fullWidth" panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" button={ @@ -85,12 +117,12 @@ export function Filtering({ onClick={() => { setFilterPopoverOpen(!filterPopoverOpen); }} - color={isInvalid ? 'danger' : 'text'} + color={isInputFilterValid ? 'text' : 'danger'} title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', { defaultMessage: 'Click to edit', })} > - {selectedColumn.filter.query || + {inputFilter.query || i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', { defaultMessage: '(empty)', })} @@ -113,16 +145,21 @@ export function Filtering({ } > - { - updateLayer(setFilter(columnId, layer, newQuery)); - }} - isInvalid={false} - onSubmit={() => {}} - /> + + {}} + /> + diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 2aa031959f5d..7cfa957f8a85 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -12,12 +12,12 @@ import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; import { InnerFieldItem, FieldItemProps } from './field_item'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl } from '@kbn/test/jest'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { IndexPattern } from './types'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { documentField } from './document_field'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; +import { FieldFormatsStart } from '../../../../../src/plugins/field_formats/public'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -29,7 +29,6 @@ describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; let indexPattern: IndexPattern; let core: ReturnType; - let data: DataPublicPluginStart; beforeEach(() => { indexPattern = { @@ -84,11 +83,15 @@ describe('IndexPattern Field Item', () => { } as IndexPattern; core = coreMock.createSetup(); - data = dataPluginMock.createStartContract(); core.http.post.mockClear(); defaultProps = { indexPattern, - data, + fieldFormats: ({ + ...fieldFormatsServiceMock.createStartContract(), + getDefaultInstance: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), + } as unknown) as FieldFormatsStart, core, highlight: '', dateRange: { @@ -112,12 +115,6 @@ describe('IndexPattern Field Item', () => { hasSuggestionForField: () => false, uiActions: uiActionsPluginMock.createStartContract(), }; - - data.fieldFormats = ({ - getDefaultInstance: jest.fn(() => ({ - convert: jest.fn((s: unknown) => JSON.stringify(s)), - })), - } as unknown) as DataPublicPluginStart['fieldFormats']; }); it('should display displayName of a field', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5ceb45203842..9c22ec9d4bb0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -36,7 +36,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { EuiHighlight } from '@elastic/eui'; import { Query, @@ -61,7 +61,7 @@ import { debouncedComponent } from '../debounced_component'; export interface FieldItemProps { core: DatasourceDataPanelProps['core']; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; field: IndexPatternField; indexPattern: IndexPattern; highlight?: string; @@ -395,7 +395,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { core, sampledValues, chartsThemeService, - data: { fieldFormats }, + fieldFormats, dropOntoWorkspace, editField, removeField, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index 6270b94abf56..b3ade3ebc48b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { IndexPattern } from './types'; import { FieldItem } from './field_item'; import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; @@ -21,7 +20,6 @@ describe('Fields Accordion', () => { let defaultProps: FieldsAccordionProps; let indexPattern: IndexPattern; let core: ReturnType; - let data: DataPublicPluginStart; let fieldProps: FieldItemSharedProps; beforeEach(() => { @@ -45,12 +43,11 @@ describe('Fields Accordion', () => { ], } as IndexPattern; core = coreMock.createSetup(); - data = dataPluginMock.createStartContract(); core.http.post.mockClear(); fieldProps = { indexPattern, - data, + fieldFormats: fieldFormatsServiceMock.createStartContract(), core, highlight: '', dateRange: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 9f5409f9837f..6a89d1770743 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -17,7 +17,7 @@ import { EuiIconTip, } from '@elastic/eui'; import classNames from 'classnames'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { IndexPatternField } from './types'; import { FieldItem } from './field_item'; import { Query, Filter } from '../../../../../src/plugins/data/public'; @@ -28,7 +28,7 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; chartsThemeService: ChartsPluginSetup['theme']; indexPattern: IndexPattern; highlight?: string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index b5844fa5cf4d..5ac9797d68db 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -5,20 +5,25 @@ * 2.0. */ -import { CoreSetup } from 'kibana/public'; +import type { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; -import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; -import { +import type { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; -import { Datasource, EditorFrameSetup } from '../types'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import type { Datasource, EditorFrameSetup } from '../types'; +import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import type { + FieldFormatsStart, + FieldFormatsSetup, +} from '../../../../../src/plugins/field_formats/public'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; + fieldFormats: FieldFormatsSetup; data: DataPublicPluginSetup; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; @@ -26,6 +31,7 @@ export interface IndexPatternDatasourceSetupPlugins { export interface IndexPatternDatasourceStartPlugins { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; uiActions: UiActionsStart; } @@ -35,7 +41,12 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { expressions, editorFrame, charts, data: dataSetup }: IndexPatternDatasourceSetupPlugins + { + fieldFormats: fieldFormatsSetup, + expressions, + editorFrame, + charts, + }: IndexPatternDatasourceSetupPlugins ) { editorFrame.registerDatasource(async () => { const { @@ -48,10 +59,11 @@ export class IndexPatternDatasource { } = await import('../async_services'); return core .getStartServices() - .then(([coreStart, { indexPatternFieldEditor, uiActions, data }]) => { - const suffixFormatter = getSuffixFormatter(data.fieldFormats.deserialize); - if (!dataSetup.fieldFormats.has(suffixFormatter.id)) { - dataSetup.fieldFormats.register([suffixFormatter]); + .then(([coreStart, { indexPatternFieldEditor, uiActions, data, fieldFormats }]) => { + const suffixFormatter = getSuffixFormatter(fieldFormats.deserialize); + if (!fieldFormats.has(suffixFormatter.id)) { + // todo: this code should be executed on setup phase. + fieldFormatsSetup.register([suffixFormatter]); } expressions.registerFunction(timeScale); expressions.registerFunction(counterRate); @@ -59,6 +71,7 @@ export class IndexPatternDatasource { expressions.registerFunction(formatColumn); return getIndexPatternDatasource({ core: coreStart, + fieldFormats, storage: new Storage(localStorage), data, charts, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 261a73287dba..b3176dbcfe40 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -20,6 +20,8 @@ import { operationDefinitionMap, getErrorMessages } from './operations'; import { createMockedFullReference } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; + jest.mock('./loader'); jest.mock('../id_generator'); jest.mock('./operations'); @@ -172,6 +174,7 @@ describe('IndexPattern Data Source', () => { storage: {} as IStorageWrapper, core: coreMock.createStart(), data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), charts: chartPluginMock.createSetupContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2cbe801a5b7b..3a2d0df88a6c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, SavedObjectReference } from 'kibana/public'; +import type { CoreStart, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; -import { +import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, DatasourceDataPanelProps, @@ -83,6 +84,7 @@ export function getIndexPatternDatasource({ core, storage, data, + fieldFormats, charts, indexPatternFieldEditor, uiActions, @@ -90,6 +92,7 @@ export function getIndexPatternDatasource({ core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; charts: ChartsPluginSetup; indexPatternFieldEditor: IndexPatternFieldEditorStart; uiActions: UiActionsStart; @@ -202,6 +205,7 @@ export function getIndexPatternDatasource({ { + updateStateOnCloseDimension: ({ state, layerId }) => { const layer = state.layers[layerId]; if (!Object.values(layer.incompleteColumns || {}).length) { return; @@ -408,7 +414,7 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, - getErrorMessages(state, layersGroups) { + getErrorMessages(state) { if (!state) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 7f718498bd95..84319d8f8e43 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -6,22 +6,17 @@ */ import './filters.scss'; - import React, { MouseEventHandler, useState } from 'react'; +import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui'; import { updateColumnParam } from '../../layer_helpers'; -import { OperationDefinition } from '../index'; -import { BaseIndexPatternColumn } from '../column_types'; +import type { OperationDefinition } from '../index'; +import type { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; -import { IndexPattern } from '../../../types'; -import { - AggFunctionsMapping, - Query, - esKuery, - esQuery, -} from '../../../../../../../../src/plugins/data/public'; +import type { IndexPattern } from '../../../types'; +import type { AggFunctionsMapping, Query } from '../../../../../../../../src/plugins/data/public'; import { queryFilterToAst } from '../../../../../../../../src/plugins/data/common'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; @@ -58,19 +53,27 @@ const defaultFilter: Filter = { label: '', }; -export const isQueryValid = (input: Query, indexPattern: IndexPattern) => { +export const validateQuery = (input: Query, indexPattern: IndexPattern) => { + let isValid = true; + let error: string | undefined; + try { if (input.language === 'kuery') { - esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(input.query), indexPattern); + toElasticsearchQuery(fromKueryExpression(input.query), indexPattern); } else { - esQuery.luceneStringToDsl(input.query); + luceneStringToDsl(input.query); } - return true; } catch (e) { - return false; + isValid = false; + error = e.message; } + + return { isValid, error }; }; +export const isQueryValid = (input: Query, indexPattern: IndexPattern) => + validateQuery(input, indexPattern).isValid; + export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn { operationType: typeof OPERATION_NAME; params: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 897777a05efc..a8ab6ef943b6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -50,7 +50,7 @@ import { FormulaIndexPatternColumn, } from './formula'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; -import { FramePublicAPI, OperationMetadata } from '../../../types'; +import { FrameDatasourceAPI, OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; import { DateRange } from '../../../../common'; @@ -280,7 +280,7 @@ interface BaseOperationDefinitionProps { label: string; newState: ( core: CoreStart, - frame: FramePublicAPI, + frame: FrameDatasourceAPI, layerId: string ) => Promise; }; @@ -437,7 +437,7 @@ interface FieldBasedOperationDefinition { label: string; newState: ( core: CoreStart, - frame: FramePublicAPI, + frame: FrameDatasourceAPI, layerId: string ) => Promise; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 4e2f69c927a1..cfe190261b53 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -22,7 +22,7 @@ import { FieldStatsResponse } from '../../../../../common'; import { AggFunctionsMapping, esQuery } from '../../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { updateColumnParam, isReferenced } from '../../layer_helpers'; -import { DataType, FramePublicAPI } from '../../../../types'; +import { DataType, FrameDatasourceAPI } from '../../../../types'; import { FiltersIndexPatternColumn, OperationDefinition, operationDefinitionMap } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesInput } from './values_input'; @@ -77,7 +77,7 @@ function getDisallowedTermsMessage( label: i18n.translate('xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel', { defaultMessage: 'Use filters', }), - newState: async (core: CoreStart, frame: FramePublicAPI, layerId: string) => { + newState: async (core: CoreStart, frame: FrameDatasourceAPI, layerId: string) => { const currentColumn = layer.columns[columnId] as TermsIndexPatternColumn; const fieldName = currentColumn.sourceField; const activeDataFieldNameMatch = diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index f326f3e3ed5f..9d5959c80749 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -22,7 +22,7 @@ import { ValuesInput } from './values_input'; import type { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; import { IndexPattern, IndexPatternLayer } from '../../../types'; -import { FramePublicAPI } from '../../../../types'; +import { FrameDatasourceAPI } from '../../../../types'; const uiSettingsMock = {} as IUiSettingsClient; @@ -1110,7 +1110,7 @@ describe('terms', () => { fromDate: '2020', toDate: '2021', }, - } as unknown) as FramePublicAPI, + } as unknown) as FrameDatasourceAPI, 'first' ); expect(newLayer.columns.col1).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 1e0d0792e132..232843171016 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -9,7 +9,8 @@ import { partition, mapValues, pickBy } from 'lodash'; import { CoreStart } from 'kibana/public'; import { Query } from 'src/plugins/data/common'; import type { - FramePublicAPI, + DatasourceFixAction, + FrameDatasourceAPI, OperationMetadata, VisualizationDimensionGroupConfig, } from '../../types'; @@ -1249,10 +1250,7 @@ export function getErrorMessages( | string | { message: string; - fixAction?: { - label: string; - newState: (frame: FramePublicAPI) => Promise; - }; + fixAction?: DatasourceFixAction; } > | undefined { @@ -1284,7 +1282,7 @@ export function getErrorMessages( fixAction: errorMessage.fixAction ? { ...errorMessage.fixAction, - newState: async (frame: FramePublicAPI) => ({ + newState: async (frame: FrameDatasourceAPI) => ({ ...state, layers: { ...state.layers, @@ -1300,10 +1298,7 @@ export function getErrorMessages( | string | { message: string; - fixAction?: { - label: string; - newState: (framePublicAPI: FramePublicAPI) => Promise; - }; + fixAction?: DatasourceFixAction; } >; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index a9e24c70ab8a..7d225d730a75 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -15,6 +15,7 @@ import type { import { operationDefinitionMap, IndexPatternColumn } from './operations'; import { getInvalidFieldMessage } from './operations/definitions/helpers'; +import { isQueryValid } from './operations/definitions/filters'; /** * Normalizes the specified operation type. (e.g. document operations @@ -68,7 +69,13 @@ export function isColumnInvalid( operationDefinitionMap ); - return (operationErrorMessages && operationErrorMessages.length > 0) || referencesHaveErrors; + const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false; + + return ( + (operationErrorMessages && operationErrorMessages.length > 0) || + referencesHaveErrors || + filterHasError + ); } function getReferencesErrors( diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 5a1e0d7fb5bd..41b487e790a0 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -24,22 +24,17 @@ export { metricChart } from '../../common/expressions'; export type { MetricState, MetricConfig } from '../../common/expressions'; export const getMetricChartRenderer = ( - formatFactory: Promise + formatFactory: FormatFactory ): ExpressionRenderDefinition => ({ name: 'lens_metric_chart_renderer', displayName: 'Metric chart', help: 'Metric chart renderer', validate: () => undefined, reuseDomNode: true, - render: async ( - domNode: Element, - config: MetricChartProps, - handlers: IInterpreterRenderHandlers - ) => { - const resolvedFormatFactory = await formatFactory; + render: (domNode: Element, config: MetricChartProps, handlers: IInterpreterRenderHandlers) => { ReactDOM.render( - + , domNode, () => { diff --git a/x-pack/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts index 484dc6140ecf..29138979ab85 100644 --- a/x-pack/plugins/lens/public/metric_visualization/index.ts +++ b/x-pack/plugins/lens/public/metric_visualization/index.ts @@ -12,7 +12,7 @@ import type { FormatFactory } from '../../common'; export interface MetricVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; } diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 3c598540cbb9..611b50b413b7 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -20,11 +20,11 @@ import { DeepPartial } from '@reduxjs/toolkit'; import { LensPublicStart } from '.'; import { visualizationTypes } from './xy_visualization/types'; import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; -import { LensAppServices } from './app_plugin/types'; +import type { LensAppServices } from './app_plugin/types'; import { DOC_TYPE } from '../common'; import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; -import { +import type { LensByValueInput, LensSavedObjectAttributes, LensByReferenceInput, @@ -33,13 +33,29 @@ import { mockAttributeService, createEmbeddableStateTransferMock, } from '../../../../src/plugins/embeddable/public/mocks'; -import { LensAttributeService } from './lens_attribute_service'; -import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; +import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/public/mocks'; +import type { LensAttributeService } from './lens_attribute_service'; +import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; import { makeConfigureStore, LensAppState, LensState } from './state_management/index'; import { getResolvedDateRange } from './utils'; import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks'; -import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types'; +import { + DatasourcePublicAPI, + Datasource, + Visualization, + FramePublicAPI, + FrameDatasourceAPI, +} from './types'; + +export function mockDatasourceStates() { + return { + testDatasource: { + state: {}, + isLoading: false, + }, + }; +} export function createMockVisualization(): jest.Mocked { return { @@ -83,9 +99,9 @@ export function createMockVisualization(): jest.Mocked { }; } -const visualizationMap = { - vis: createMockVisualization(), - vis2: createMockVisualization(), +export const visualizationMap = { + testVis: createMockVisualization(), + testVis2: createMockVisualization(), }; export type DatasourceMock = jest.Mocked & { @@ -134,7 +150,7 @@ export function createMockDatasource(id: string): DatasourceMock { const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); -const datasourceMap = { +export const datasourceMap = { testDatasource2: mockDatasource2, testDatasource: mockDatasource, }; @@ -148,12 +164,18 @@ export function createExpressionRendererMock(): jest.Mock< export type FrameMock = jest.Mocked; export function createMockFramePublicAPI(): FrameMock { + return { + datasourceLayers: {}, + }; +} + +export type FrameDatasourceMock = jest.Mocked; +export function createMockFrameDatasourceAPI(): FrameDatasourceMock { return { datasourceLayers: {}, dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, filters: [], - searchSessionId: 'sessionId', }; } @@ -370,6 +392,7 @@ export function makeDefaultServices( getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), }, data: mockDataPlugin(sessionIdSubject), + fieldFormats: fieldFormatsServiceMock.createStartContract(), storage: { get: jest.fn(), set: jest.fn(), @@ -393,12 +416,7 @@ export const defaultState = { state: {}, activeId: 'testVis', }, - datasourceStates: { - testDatasource: { - isLoading: false, - state: '', - }, - }, + datasourceStates: mockDatasourceStates(), }; export function makeLensStore({ diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index ce36f88b2805..c1b9f4c799e6 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -22,7 +22,7 @@ import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plug export { pie } from '../../common/expressions'; export const getPieRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; }): ExpressionRenderDefinition => ({ @@ -33,20 +33,16 @@ export const getPieRenderer = (dependencies: { help: '', validate: () => undefined, reuseDomNode: true, - render: async ( - domNode: Element, - config: PieExpressionProps, - handlers: IInterpreterRenderHandlers - ) => { + render: (domNode: Element, config: PieExpressionProps, handlers: IInterpreterRenderHandlers) => { const onClickValue = (data: LensFilterEvent['data']) => { handlers.event({ name: 'filter', data }); }; - const formatFactory = await dependencies.formatFactory; + ReactDOM.render( ; + formatFactory: FormatFactory; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ae8aa268c058..e0a484897423 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -7,6 +7,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; +import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; @@ -57,7 +58,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, FormatFactory, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import type { EditorFrameStart, VisualizationType } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -77,6 +78,7 @@ export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; + fieldFormats: FieldFormatsSetup; embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; @@ -86,6 +88,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; expressions: ExpressionsStart; navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; @@ -158,6 +161,7 @@ export class LensPlugin { urlForwarding, expressions, data, + fieldFormats, embeddable, visualizations, charts, @@ -170,14 +174,25 @@ export class LensPlugin { const [coreStart, startDependencies] = await core.getStartServices(); return getLensAttributeService(coreStart, startDependencies); }; + const getStartServices = async (): Promise => { const [coreStart, deps] = await core.getStartServices(); - this.initParts(core, data, embeddable, charts, expressions, usageCollection); + + this.initParts( + core, + data, + embeddable, + charts, + expressions, + usageCollection, + fieldFormats, + deps.fieldFormats.deserialize + ); + return { attributeService: await this.attributeService!(), capabilities: coreStart.application.capabilities, coreHttp: coreStart.http, - executionContext: coreStart.executionContext, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, @@ -211,7 +226,19 @@ export class LensPlugin { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { - await this.initParts(core, data, embeddable, charts, expressions, usageCollection); + const [, deps] = await core.getStartServices(); + + await this.initParts( + core, + data, + embeddable, + charts, + expressions, + usageCollection, + fieldFormats, + deps.fieldFormats.deserialize + ); + const { mountApp, stopReportManager } = await import('./async_services'); this.stopReportManager = stopReportManager; await ensureDefaultIndexPattern(); @@ -246,7 +273,9 @@ export class LensPlugin { embeddable: EmbeddableSetup | undefined, charts: ChartsPluginSetup, expressions: ExpressionsServiceSetup, - usageCollection: UsageCollectionSetup | undefined + usageCollection: UsageCollectionSetup | undefined, + fieldFormats: FieldFormatsSetup, + formatFactory: FormatFactory ) { const { DatatableVisualization, @@ -278,11 +307,10 @@ export class LensPlugin { PieVisualizationPluginSetupPlugins = { expressions, data, + fieldFormats, charts, editorFrame: editorFrameSetupInterface, - formatFactory: core - .getStartServices() - .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), + formatFactory, }; this.indexpatternDatasource.setup(core, dependencies); this.xyVisualization.setup(core, dependencies); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 3564785aa570..97dc2e45c96d 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -405,4 +405,15 @@ describe('getContrastColor', () => { expect(getContrastColor('#fff', true)).toBe('#000000'); expect(getContrastColor('#fff', false)).toBe('#000000'); }); + + it('should take into account background color if the primary color is opaque', () => { + expect(getContrastColor('rgba(0,0,0,0)', true)).toBe('#ffffff'); + expect(getContrastColor('rgba(0,0,0,0)', false)).toBe('#000000'); + expect(getContrastColor('#00000000', true)).toBe('#ffffff'); + expect(getContrastColor('#00000000', false)).toBe('#000000'); + expect(getContrastColor('#ffffff00', true)).toBe('#ffffff'); + expect(getContrastColor('#ffffff00', false)).toBe('#000000'); + expect(getContrastColor('rgba(255,255,255,0)', true)).toBe('#ffffff'); + expect(getContrastColor('rgba(255,255,255,0)', false)).toBe('#000000'); + }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index 8cd0a6cf4900..b2969565f539 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -294,7 +294,12 @@ export function getColorStops( export function getContrastColor(color: string, isDarkTheme: boolean) { const darkColor = isDarkTheme ? euiDarkVars.euiColorInk : euiLightVars.euiColorInk; const lightColor = isDarkTheme ? euiDarkVars.euiColorGhost : euiLightVars.euiColorGhost; - return isColorDark(...chroma(color).rgb()) ? lightColor : darkColor; + const backgroundColor = isDarkTheme + ? euiDarkVars.euiPageBackgroundColor + : euiLightVars.euiPageBackgroundColor; + const finalColor = + chroma(color).alpha() < 1 ? chroma.blend(backgroundColor, color, 'overlay') : chroma(color); + return isColorDark(...finalColor.rgb()) ? lightColor : darkColor; } /** diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index b06dc73857cf..a23a040de236 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -14,6 +14,7 @@ import { optimizingMiddleware } from './optimizing_middleware'; import { LensState, LensStoreDeps } from './types'; import { initMiddleware } from './init_middleware'; export * from './types'; +export * from './selectors'; export const reducer = { lens: lensSlice.reducer, diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts new file mode 100644 index 000000000000..360ca48c2d27 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -0,0 +1,153 @@ +/* + * 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 { createSelector } from '@reduxjs/toolkit'; +import { SavedObjectReference } from 'kibana/server'; +import { LensState } from './types'; +import { extractFilterReferences } from '../persistence'; +import { Datasource, DatasourceMap, VisualizationMap } from '../types'; +import { createDatasourceLayers } from '../editor_frame_service/editor_frame'; + +export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc; +export const selectQuery = (state: LensState) => state.lens.query; +export const selectSearchSessionId = (state: LensState) => state.lens.searchSessionId; +export const selectFilters = (state: LensState) => state.lens.filters; +export const selectResolvedDateRange = (state: LensState) => state.lens.resolvedDateRange; +export const selectVisualization = (state: LensState) => state.lens.visualization; +export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview; +export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; +export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; +export const selectActiveData = (state: LensState) => state.lens.activeData; +export const selectIsFullscreenDatasource = (state: LensState) => + Boolean(state.lens.isFullscreenDatasource); + +export const selectExecutionContext = createSelector( + [selectQuery, selectFilters, selectResolvedDateRange], + (query, filters, dateRange) => ({ + dateRange, + query, + filters, + }) +); + +export const selectExecutionContextSearch = createSelector(selectExecutionContext, (res) => ({ + query: res.query, + timeRange: { + from: res.dateRange.fromDate, + to: res.dateRange.toDate, + }, + filters: res.filters, +})); + +const selectDatasourceMap = (state: LensState, datasourceMap: DatasourceMap) => datasourceMap; + +const selectVisualizationMap = ( + state: LensState, + datasourceMap: DatasourceMap, + visualizationMap: VisualizationMap +) => visualizationMap; + +export const selectSavedObjectFormat = createSelector( + [ + selectPersistedDoc, + selectVisualization, + selectDatasourceStates, + selectQuery, + selectFilters, + selectActiveDatasourceId, + selectDatasourceMap, + selectVisualizationMap, + ], + ( + persistedDoc, + visualization, + datasourceStates, + query, + filters, + activeDatasourceId, + datasourceMap, + visualizationMap + ) => { + const activeVisualization = + visualization.state && visualization.activeId && visualizationMap[visualization.activeId]; + const activeDatasource = + datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading + ? datasourceMap[activeDatasourceId] + : undefined; + + if (!activeDatasource || !activeVisualization) { + return; + } + + const activeDatasources: Record = Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ); + + const persistibleDatasourceStates: Record = {}; + const references: SavedObjectReference[] = []; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + datasourceStates[id].state + ); + persistibleDatasourceStates[id] = persistableState; + references.push(...savedObjectReferences); + }); + + const { persistableFilters, references: filterReferences } = extractFilterReferences(filters); + + references.push(...filterReferences); + + return { + savedObjectId: persistedDoc?.savedObjectId, + title: persistedDoc?.title || '', + description: persistedDoc?.description, + visualizationType: visualization.activeId, + type: 'lens', + references, + state: { + visualization: visualization.state, + query, + filters: persistableFilters, + datasourceStates: persistibleDatasourceStates, + }, + }; + } +); + +export const selectCurrentVisualization = createSelector( + [selectVisualization, selectStagedPreview], + (visualization, stagedPreview) => (stagedPreview ? stagedPreview.visualization : visualization) +); + +export const selectCurrentDatasourceStates = createSelector( + [selectDatasourceStates, selectStagedPreview], + (datasourceStates, stagedPreview) => + stagedPreview ? stagedPreview.datasourceStates : datasourceStates +); + +export const selectAreDatasourcesLoaded = createSelector( + selectDatasourceStates, + (datasourceStates) => + Object.values(datasourceStates).every(({ isLoading }) => isLoading === false) +); + +export const selectDatasourceLayers = createSelector( + [selectDatasourceStates, selectDatasourceMap], + (datasourceStates, datasourceMap) => createDatasourceLayers(datasourceStates, datasourceMap) +); + +export const selectFramePublicAPI = createSelector( + [selectDatasourceStates, selectActiveData, selectDatasourceMap], + (datasourceStates, activeData, datasourceMap) => ({ + datasourceLayers: createDatasourceLayers(datasourceStates, datasourceMap), + activeData, + }) +); diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts index cc3e46b71fbf..386e0d88e181 100644 --- a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts @@ -10,7 +10,10 @@ import moment from 'moment'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { setState, LensDispatch } from '.'; import { LensAppState } from './types'; -import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils'; +import { getResolvedDateRange, containsDynamicMath } from '../utils'; + +const TIME_LAG_PERCENTAGE_LIMIT = 0.02; +const TIME_LAG_MIN_LIMIT = 10000; // for a small timerange to avoid infinite data refresh timelag minimum is TIME_LAG_ABSOLUTE ms /** * checks if TIME_LAG_PERCENTAGE_LIMIT passed to renew searchSessionId @@ -48,8 +51,8 @@ function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) { // calculate lag of managed "now" for date math const nowDiff = Date.now() - data.nowProvider.get().valueOf(); - // if the lag is signifcant, start a new session to clear the cache - if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { + // if the lag is significant, start a new session to clear the cache + if (nowDiff > Math.max(timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT, TIME_LAG_MIN_LIMIT)) { dispatch( setState({ searchSessionId: data.search.session.start(), diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 8816ef27238c..7321f72386b4 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -15,12 +15,15 @@ import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; import { DatasourceMap, VisualizationMap } from '../types'; +export interface VisualizationState { + activeId: string | null; + state: unknown; +} + +export type DatasourceStates = Record; export interface PreviewState { - visualization: { - activeId: string | null; - state: unknown; - }; - datasourceStates: Record; + visualization: VisualizationState; + datasourceStates: DatasourceStates; } export interface EditorFrameState extends PreviewState { activeDatasourceId: string | null; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 91b16d2bcbc1..db17154e3bbd 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -256,6 +256,11 @@ export interface Datasource { getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; } +export interface DatasourceFixAction { + label: string; + newState: (frame: FrameDatasourceAPI) => Promise; +} + /** * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ @@ -516,10 +521,11 @@ export interface FramePublicAPI { * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; +} +export interface FrameDatasourceAPI extends FramePublicAPI { dateRange: DateRange; query: Query; filters: Filter[]; - searchSessionId: string; } /** diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index b8ee5e4a0c26..b7dd3ed3733c 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -10,11 +10,10 @@ import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plu import { IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { SavedObjectReference } from 'kibana/public'; -import { Filter, Query } from 'src/plugins/data/public'; import { uniq } from 'lodash'; import { Document } from './persistence/saved_object_store'; import { Datasource, DatasourceMap } from './types'; -import { extractFilterReferences } from './persistence'; +import { DatasourceStates } from './state_management'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { @@ -36,8 +35,6 @@ export function containsDynamicMath(dateMathString: string) { return dateMathString.includes('now'); } -export const TIME_LAG_PERCENTAGE_LIMIT = 0.02; - export function getTimeZone(uiSettings: IUiSettingsClient) { const configuredTimeZone = uiSettings.get('dateFormat:tz'); if (configuredTimeZone === 'Browser') { @@ -59,66 +56,12 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; -export interface GetIndexPatternsObjects { - activeDatasources: Record; - datasourceStates: Record; - visualization: { - activeId: string | null; - state: unknown; - }; - filters: Filter[]; - query: Query; - title: string; - description?: string; - persistedId?: string; -} - -export function getSavedObjectFormat({ - activeDatasources, - datasourceStates, - visualization, - filters, - query, - title, - description, - persistedId, -}: GetIndexPatternsObjects): Document { - const persistibleDatasourceStates: Record = {}; - const references: SavedObjectReference[] = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( - datasourceStates[id].state - ); - persistibleDatasourceStates[id] = persistableState; - references.push(...savedObjectReferences); - }); - - const { persistableFilters, references: filterReferences } = extractFilterReferences(filters); - - references.push(...filterReferences); - - return { - savedObjectId: persistedId, - title, - description, - type: 'lens', - visualizationType: visualization.activeId, - state: { - datasourceStates: persistibleDatasourceStates, - visualization: visualization.state, - query, - filters: persistableFilters, - }, - references, - }; -} - export function getIndexPatternsIds({ activeDatasources, datasourceStates, }: { activeDatasources: Record; - datasourceStates: Record; + datasourceStates: DatasourceStates; }): string[] { const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 23b251b76e95..9ef87fe4f48d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -123,7 +123,7 @@ export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { } export const getXyChartRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginStart['theme']; chartsActiveCursorService: ChartsPluginStart['activeCursor']; paletteService: PaletteRegistry; @@ -148,12 +148,12 @@ export const getXyChartRenderer = (dependencies: { const onSelectRange = (data: LensBrushEvent['data']) => { handlers.event({ name: 'brush', data }); }; - const formatFactory = await dependencies.formatFactory; + ReactDOM.render( ; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } @@ -41,7 +41,7 @@ export class XyVisualization { getXyChartRenderer, getXyVisualization, } = await import('../async_services'); - const [, { data, charts }] = await core.getStartServices(); + const [, { charts, fieldFormats }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => yAxisConfig); @@ -62,7 +62,7 @@ export class XyVisualization { timeZone: getTimeZone(core.uiSettings), }) ); - return getXyVisualization({ paletteService: palettes, data }); + return getXyVisualization({ paletteService: palettes, fieldFormats }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index f43b633f4a71..621e2897a105 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -11,12 +11,12 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { getXyVisualization } from './xy_visualization'; import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), - data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index fd80b9d96d30..ef97e2622ee8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -13,7 +13,7 @@ import type { SeriesType, XYLayerConfig } from '../../common/expressions'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; function exampleState(): State { return { @@ -32,11 +32,11 @@ function exampleState(): State { }; } const paletteServiceMock = chartPluginMock.createPaletteRegistry(); -const dataMock = dataPluginMock.createStartContract(); +const fieldFormatsMock = fieldFormatsServiceMock.createStartContract(); const xyVisualization = getXyVisualization({ paletteService: paletteServiceMock, - data: dataMock, + fieldFormats: fieldFormatsMock, }); describe('xy_visualization', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 40caed718819..799246ef26b8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -12,7 +12,7 @@ import { Position } from '@elastic/charts'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import type { @@ -87,10 +87,10 @@ function getDescription(state?: State) { export const getXyVisualization = ({ paletteService, - data, + fieldFormats, }: { paletteService: PaletteRegistry; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; }): Visualization => ({ id: 'lnsXY', @@ -190,7 +190,7 @@ export const getXyVisualization = ({ const colorAssignments = getColorAssignments( state.layers, { tables: frame.activeData }, - data.fieldFormats.deserialize + fieldFormats.deserialize ); mappedAccessors = getAccessorColorConfig( colorAssignments, @@ -336,7 +336,7 @@ export const getXyVisualization = ({ , diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 87165d64625e..924b87647fce 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -6,19 +6,19 @@ */ import { getSuggestions } from './xy_suggestions'; -import { TableSuggestionColumn, VisualizationSuggestion, TableSuggestion } from '../types'; +import type { TableSuggestionColumn, VisualizationSuggestion, TableSuggestion } from '../types'; import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { PaletteOutput } from 'src/plugins/charts/public'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import type { PaletteOutput } from 'src/plugins/charts/public'; jest.mock('../id_generator'); const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), - data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), }); describe('xy_suggestions', () => { diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts index 4f21378cc811..6f1ec38ea951 100644 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts @@ -6,7 +6,7 @@ */ import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { DOC_TYPE } from '../../common'; import { commonRemoveTimezoneDateHistogramParam, @@ -25,7 +25,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return ({ ...lensState, attributes: migratedLensState, - } as unknown) as SerializableState; + } as unknown) as SerializableRecord; }, '7.14.0': (state) => { const lensState = (state as unknown) as { attributes: LensDocShape713 }; @@ -33,7 +33,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return ({ ...lensState, attributes: migratedLensState, - } as unknown) as SerializableState; + } as unknown) as SerializableRecord; }, }, }; diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts new file mode 100644 index 000000000000..8f8e6131b072 --- /dev/null +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + pie, + xyChart, + timeScale, + counterRate, + metricChart, + yAxisConfig, + layerConfig, + formatColumn, + legendConfig, + renameColumns, + gridlinesConfig, + datatableColumn, + tickLabelsConfig, + axisTitlesVisibilityConfig, + getDatatable, +} from '../../common/expressions'; +import type { PluginStartContract } from '../plugin'; +import type { + ExecutionContext, + ExpressionsServerSetup, +} from '../../../../../src/plugins/expressions/server'; + +const getUiSettings = (coreStart: CoreStart, kibanaRequest: KibanaRequest) => + coreStart.uiSettings.asScopedToClient(coreStart.savedObjects.getScopedClient(kibanaRequest)); + +export const setupExpressions = ( + core: CoreSetup, + expressions: ExpressionsServerSetup +) => { + const getFormatFactory = async (context: ExecutionContext) => { + const [coreStart, { fieldFormats: fieldFormatsStart }] = await core.getStartServices(); + const kibanaRequest = context.getKibanaRequest?.(); + + if (!kibanaRequest) { + throw new Error('"lens_datatable" expression function requires a KibanaRequest to execute'); + } + + const fieldFormats = await fieldFormatsStart.fieldFormatServiceFactory( + getUiSettings(coreStart, kibanaRequest) + ); + + return fieldFormats.deserialize; + }; + + [ + pie, + xyChart, + timeScale, + counterRate, + metricChart, + yAxisConfig, + layerConfig, + formatColumn, + legendConfig, + renameColumns, + gridlinesConfig, + datatableColumn, + tickLabelsConfig, + axisTitlesVisibilityConfig, + getDatatable(getFormatFactory), + ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); +}; diff --git a/x-pack/plugins/lens/server/expressions/index.ts b/x-pack/plugins/lens/server/expressions/index.ts new file mode 100644 index 000000000000..b726ba94ae5b --- /dev/null +++ b/x-pack/plugins/lens/server/expressions/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 { setupExpressions } from './expressions'; diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index b47019fa54ec..f0ee801ece89 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -10,6 +10,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { PluginStart as DataPluginStart } from 'src/plugins/data/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { setupRoutes } from './routes'; import { @@ -20,6 +21,7 @@ import { import { setupSavedObjects } from './saved_objects'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; import { lensEmbeddableFactory } from './embeddable/lens_embeddable_factory'; +import { setupExpressions } from './expressions'; export interface PluginSetupContract { usageCollection?: UsageCollectionSetup; @@ -30,6 +32,7 @@ export interface PluginSetupContract { export interface PluginStartContract { taskManager?: TaskManagerStartContract; + fieldFormats: FieldFormatsStart; data: DataPluginStart; } @@ -44,6 +47,8 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { setup(core: CoreSetup, plugins: PluginSetupContract) { setupSavedObjects(core); setupRoutes(core, this.initializerContext.logger.get()); + setupExpressions(core, plugins.expressions); + if (plugins.usageCollection && plugins.taskManager) { registerLensUsageCollector( plugins.usageCollection, diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 6c4d3631a12f..04d3838df206 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -1,42 +1,41 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "*.ts", - "common/**/*", - "public/**/*", - "server/**/*", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../task_manager/tsconfig.json" }, - { "path": "../global_search/tsconfig.json"}, - { "path": "../saved_objects_tagging/tsconfig.json"}, - { "path": "../../../src/plugins/data/tsconfig.json"}, - { "path": "../../../src/plugins/index_pattern_field_editor/tsconfig.json"}, - { "path": "../../../src/plugins/charts/tsconfig.json"}, - { "path": "../../../src/plugins/expressions/tsconfig.json"}, - { "path": "../../../src/plugins/navigation/tsconfig.json" }, - { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../../src/plugins/visualizations/tsconfig.json" }, - { "path": "../../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json"}, - { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, - { "path": "../../../src/plugins/field_formats/tsconfig.json"} - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../global_search/tsconfig.json"}, + { "path": "../saved_objects_tagging/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/index_pattern_field_editor/tsconfig.json"}, + { "path": "../../../src/plugins/charts/tsconfig.json"}, + { "path": "../../../src/plugins/expressions/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + { "path": "../../../src/plugins/field_formats/tsconfig.json"} + ] +} diff --git a/x-pack/plugins/license_api_guard/tsconfig.json b/x-pack/plugins/license_api_guard/tsconfig.json index 1b6ea789760d..123e73a9e816 100644 --- a/x-pack/plugins/license_api_guard/tsconfig.json +++ b/x-pack/plugins/license_api_guard/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/license_management/tsconfig.json b/x-pack/plugins/license_management/tsconfig.json index e6cb0101ee83..4384a9a0efd9 100644 --- a/x-pack/plugins/license_management/tsconfig.json +++ b/x-pack/plugins/license_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/licensing/tsconfig.json b/x-pack/plugins/licensing/tsconfig.json index 6118bcd81d34..d8855fcd6591 100644 --- a/x-pack/plugins/licensing/tsconfig.json +++ b/x-pack/plugins/licensing/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/lists/tsconfig.json b/x-pack/plugins/lists/tsconfig.json index 9025a1640f8b..691c5243d9db 100644 --- a/x-pack/plugins/lists/tsconfig.json +++ b/x-pack/plugins/lists/tsconfig.json @@ -1,23 +1,22 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json"}, - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json index 6f21cfdb0b19..5a13e8ca7159 100644 --- a/x-pack/plugins/logstash/tsconfig.json +++ b/x-pack/plugins/logstash/tsconfig.json @@ -1,26 +1,25 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json"}, - { "path": "../../../src/plugins/management/tsconfig.json"}, + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json"}, + { "path": "../../../src/plugins/management/tsconfig.json"}, - { "path": "../features/tsconfig.json" }, - { "path": "../licensing/tsconfig.json"}, - { "path": "../monitoring/tsconfig.json"}, - { "path": "../security/tsconfig.json"}, - ] - } + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json"}, + { "path": "../monitoring/tsconfig.json"}, + { "path": "../security/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 82be83dad43f..4755781147e5 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { GeoJsonProperties } from 'geojson'; import { i18n } from '@kbn/i18n'; +import type { Filter } from 'src/plugins/data/public'; import { EMPTY_FEATURE_COLLECTION, FIELD_ORIGIN, @@ -202,6 +203,15 @@ export class ESGeoLineSource extends AbstractESAggSource { terms: addFieldToDSL(termsAgg, splitField), }, }); + if (splitField.type === 'string') { + const entityIsNotEmptyFilter = esFilters.buildPhraseFilter(splitField, '', indexPattern); + entityIsNotEmptyFilter.meta.negate = true; + entitySearchSource.setField('filter', [ + ...(entitySearchSource.getField('filter') as Filter[]), + entityIsNotEmptyFilter, + ]); + } + const entityResp = await this._runEsQuery({ requestId: `${this.getId()}_entities`, requestName: i18n.translate('xpack.maps.source.esGeoLine.entityRequestName', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 55eed588b884..b63a821cdb0c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -9,8 +9,9 @@ import _ from 'lodash'; import React, { ReactElement } from 'react'; import rison from 'rison-node'; import { i18n } from '@kbn/i18n'; -import { IFieldType, IndexPattern } from 'src/plugins/data/public'; +import type { Filter, IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { AbstractESSource } from '../es_source'; import { getHttp, getSearchService, getTimeFilter } from '../../../kibana_services'; import { @@ -311,6 +312,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }, }, }); + if (topHitsSplitField.type === 'string') { + const entityIsNotEmptyFilter = esFilters.buildPhraseFilter( + topHitsSplitField, + '', + indexPattern + ); + entityIsNotEmptyFilter.meta.negate = true; + searchSource.setField('filter', [ + ...(searchSource.getField('filter') as Filter[]), + entityIsNotEmptyFilter, + ]); + } const resp = await this._runEsQuery({ requestId: this.getId(), diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 7dc816f6e1b6..4dbf5f16b067 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -71,6 +71,7 @@ export interface IVectorSource extends ISource { supportsFeatureEditing(): Promise; addFeature(geometry: Geometry | Position[]): Promise; deleteFeature(featureId: string): Promise; + isFilterByMapBounds(): boolean; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index d07b39be6f6a..e0d630994566 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -// @ts-expect-error import { Join } from './resources/join'; import { ILayer } from '../../../classes/layers/layer'; import { JoinDescriptor } from '../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap similarity index 90% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap index a9a1afabfc19..91eec4d8aac2 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap @@ -43,13 +43,18 @@ exports[`Should render default props 1`] = ` values={Object {}} /> - - - +
`; @@ -99,12 +104,7 @@ exports[`Should render metrics expression for metrics 1`] = ` void; + onRemove: () => void; + leftFields: JoinField[]; + leftSourceName: string; +} + +interface State { + rightFields: IFieldType[]; + indexPattern?: IndexPattern; + loadError?: string; +} + +export class Join extends Component { + private _isMounted = false; + + state: State = { + rightFields: [], indexPattern: undefined, loadError: undefined, }; @@ -36,7 +61,7 @@ export class Join extends Component { this._isMounted = false; } - async _loadRightFields(indexPatternId) { + async _loadRightFields(indexPatternId: string) { if (!indexPatternId) { return; } @@ -66,21 +91,26 @@ export class Join extends Component { }); } - _onLeftFieldChange = (leftField) => { + _onLeftFieldChange = (leftField: string) => { this.props.onChange({ - leftField: leftField, + leftField, right: this.props.join.right, }); }; - _onRightSourceChange = ({ indexPatternId, indexPatternTitle }) => { + _onRightSourceChange = ({ + indexPatternId, + indexPatternTitle, + }: { + indexPatternId: string; + indexPatternTitle: string; + }) => { this.setState({ - rightFields: undefined, + rightFields: [], loadError: undefined, }); this._loadRightFields(indexPatternId); - // eslint-disable-next-line no-unused-vars - const { term, ...restOfRight } = this.props.join.right; + const { term, ...restOfRight } = this.props.join.right as ESTermSourceDescriptor; this.props.onChange({ leftField: this.props.join.leftField, right: { @@ -88,74 +118,74 @@ export class Join extends Component { indexPatternId, indexPatternTitle, type: SOURCE_TYPES.ES_TERM_SOURCE, - }, + } as ESTermSourceDescriptor, }); }; - _onRightFieldChange = (term) => { + _onRightFieldChange = (term?: string) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, term, - }, + } as ESTermSourceDescriptor, }); }; - _onRightSizeChange = (size) => { + _onRightSizeChange = (size: number) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, size, - }, + } as ESTermSourceDescriptor, }); }; - _onMetricsChange = (metrics) => { + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, metrics, - }, + } as ESTermSourceDescriptor, }); }; - _onWhereQueryChange = (whereQuery) => { + _onWhereQueryChange = (whereQuery?: Query) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, whereQuery, - }, + } as ESTermSourceDescriptor, }); }; - _onApplyGlobalQueryChange = (applyGlobalQuery) => { + _onApplyGlobalQueryChange = (applyGlobalQuery: boolean) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, applyGlobalQuery, - }, + } as ESTermSourceDescriptor, }); }; - _onApplyGlobalTimeChange = (applyGlobalTime) => { + _onApplyGlobalTimeChange = (applyGlobalTime: boolean) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, applyGlobalTime, - }, + } as ESTermSourceDescriptor, }); }; render() { const { join, onRemove, leftFields, leftSourceName } = this.props; const { rightFields, indexPattern } = this.state; - const right = _.get(join, 'right', {}); + const right = _.get(join, 'right', {}) as ESTermSourceDescriptor; const rightSourceName = right.indexPatternTitle ? right.indexPatternTitle : right.indexPatternId; @@ -168,7 +198,7 @@ export class Join extends Component { metricsExpression = ( @@ -176,7 +206,9 @@ export class Join extends Component { ); globalFilterCheckbox = ( diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx similarity index 87% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx index 58e3e3aac0d6..f2073a9f6e65 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx @@ -7,30 +7,64 @@ import _ from 'lodash'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiPopover, EuiPopoverTitle, EuiExpression, EuiFormRow, EuiComboBox, + EuiComboBoxOptionOption, EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { IFieldType } from 'src/plugins/data/public'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../common/constants'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { ValidatedNumberInput } from '../../../../components/validated_number_input'; -import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; import { getIndexPatternService, getIndexPatternSelectComponent, } from '../../../../kibana_services'; +import type { JoinField } from '../join_editor'; + +interface Props { + // Left source props (static - can not change) + leftSourceName?: string; + + // Left field props + leftValue?: string; + leftFields: JoinField[]; + onLeftFieldChange: (leftField: string) => void; -export class JoinExpression extends Component { - state = { + // Right source props + rightSourceIndexPatternId: string; + rightSourceName: string; + onRightSourceChange: ({ + indexPatternId, + indexPatternTitle, + }: { + indexPatternId: string; + indexPatternTitle: string; + }) => void; + + // Right field props + rightValue: string; + rightSize?: number; + rightFields: IFieldType[]; + onRightFieldChange: (term?: string) => void; + onRightSizeChange: (size: number) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +export class JoinExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -46,7 +80,11 @@ export class JoinExpression extends Component { }); }; - _onRightSourceChange = async (indexPatternId) => { + _onRightSourceChange = async (indexPatternId?: string) => { + if (!indexPatternId || indexPatternId.length === 0) { + return; + } + try { const indexPattern = await getIndexPatternService().get(indexPatternId); this.props.onRightSourceChange({ @@ -58,7 +96,7 @@ export class JoinExpression extends Component { } }; - _onLeftFieldChange = (selectedFields) => { + _onLeftFieldChange = (selectedFields: Array>) => { this.props.onLeftFieldChange(_.get(selectedFields, '[0].value.name', null)); }; @@ -246,7 +284,9 @@ export class JoinExpression extends Component { })} > @@ -263,33 +303,6 @@ export class JoinExpression extends Component { } } -JoinExpression.propTypes = { - // Left source props (static - can not change) - leftSourceName: PropTypes.string, - - // Left field props - leftValue: PropTypes.string, - leftFields: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }) - ), - onLeftFieldChange: PropTypes.func.isRequired, - - // Right source props - rightSourceIndexPatternId: PropTypes.string, - rightSourceName: PropTypes.string, - onRightSourceChange: PropTypes.func.isRequired, - - // Right field props - rightValue: PropTypes.string, - rightSize: PropTypes.number, - rightFields: PropTypes.array, - onRightFieldChange: PropTypes.func.isRequired, - onRightSizeChange: PropTypes.func.isRequired, -}; - function getSelectFieldPlaceholder() { return i18n.translate('xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder', { defaultMessage: 'Select field', diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx similarity index 67% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx index 8140d7a36ea9..aa696383fa37 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx @@ -8,9 +8,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricsExpression } from './metrics_expression'; +import { AGG_TYPE } from '../../../../../common/constants'; const defaultProps = { onChange: () => {}, + metrics: [{ type: AGG_TYPE.COUNT }], + rightFields: [], }; test('Should render default props', () => { @@ -23,11 +26,10 @@ test('Should render metrics expression for metrics', () => { const component = shallow( ); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx similarity index 80% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx index 581cb75b4500..899430f3c2f2 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx @@ -6,7 +6,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -16,12 +15,24 @@ import { EuiFormHelpText, } from '@elastic/eui'; -import { MetricsEditor } from '../../../../components/metrics_editor'; +import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MetricsEditor } from '../../../../components/metrics_editor'; import { AGG_TYPE } from '../../../../../common/constants'; +import { AggDescriptor, FieldedAggDescriptor } from '../../../../../common/descriptor_types'; + +interface Props { + metrics: AggDescriptor[]; + rightFields: IFieldType[]; + onChange: (metrics: AggDescriptor[]) => void; +} -export class MetricsExpression extends Component { - state = { +interface State { + isPopoverOpen: boolean; +} + +export class MetricsExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -61,23 +72,23 @@ export class MetricsExpression extends Component { render() { const metricExpressions = this.props.metrics - .filter(({ type, field }) => { - if (type === AGG_TYPE.COUNT) { + .filter((metric: AggDescriptor) => { + if (metric.type === AGG_TYPE.COUNT) { return true; } - if (field) { + if ((metric as FieldedAggDescriptor).field) { return true; } return false; }) - .map(({ type, field }) => { + .map((metric: AggDescriptor) => { // do not use metric label so field and aggregation are not obscured. - if (type === AGG_TYPE.COUNT) { - return 'count'; + if (metric.type === AGG_TYPE.COUNT) { + return AGG_TYPE.COUNT; } - return `${type} ${field}`; + return `${metric.type} ${(metric as FieldedAggDescriptor).field}`; }); const useMetricDescription = i18n.translate( 'xpack.maps.layerPanel.metricsExpression.useMetricsDescription', @@ -101,7 +112,7 @@ export class MetricsExpression extends Component { onClick={this._togglePopover} description={useMetricDescription} uppercase={false} - value={metricExpressions.length > 0 ? metricExpressions.join(', ') : 'count'} + value={metricExpressions.length > 0 ? metricExpressions.join(', ') : AGG_TYPE.COUNT} /> } > @@ -124,13 +135,3 @@ export class MetricsExpression extends Component { ); } } - -MetricsExpression.propTypes = { - metrics: PropTypes.array, - rightFields: PropTypes.array, - onChange: PropTypes.func.isRequired, -}; - -MetricsExpression.defaultProps = { - metrics: [{ type: AGG_TYPE.COUNT }], -}; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx similarity index 86% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx index 93ff3c95d184..16cef0d5bdad 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx @@ -9,10 +9,22 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, Query } from 'src/plugins/data/public'; +import { APP_ID } from '../../../../../common/constants'; import { getData } from '../../../../kibana_services'; -export class WhereExpression extends Component { - state = { +interface Props { + indexPattern: IndexPattern; + onChange: (whereQuery?: Query) => void; + whereQuery?: Query; +} + +interface State { + isPopoverOpen: boolean; +} + +export class WhereExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -28,7 +40,7 @@ export class WhereExpression extends Component { }); }; - _onQueryChange = ({ query }) => { + _onQueryChange = ({ query }: { query?: Query }) => { this.props.onChange(query); this._closePopover(); }; @@ -73,6 +85,7 @@ export class WhereExpression extends Component { /> { }), }); } + const source = this.props.layer.getSource(); + if ( + typeof source.isFilterByMapBounds === 'function' && + (source as IVectorSource).isFilterByMapBounds() + ) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingBoundsFilter', { + defaultMessage: 'Results narrowed by visible map area', + }), + }); + } } return { diff --git a/x-pack/plugins/maps/public/locators.test.ts b/x-pack/plugins/maps/public/locators.test.ts index d6e82d1cdb60..838d27212032 100644 --- a/x-pack/plugins/maps/public/locators.test.ts +++ b/x-pack/plugins/maps/public/locators.test.ts @@ -8,7 +8,7 @@ import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; import { esFilters } from '../../../../src/plugins/data/public'; import { MapsAppLocatorDefinition } from './locators'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; import { LayerDescriptor } from '../common/descriptor_types'; const MAP_ID: string = '2c9c1f60-1909-11e9-919b-ffe5949a18d2'; @@ -65,7 +65,7 @@ describe('visualize url generator', () => { }, ]; const location = await locator.getLocation({ - initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableRecord, }); expect(location).toMatchObject({ diff --git a/x-pack/plugins/maps/public/locators.ts b/x-pack/plugins/maps/public/locators.ts index 7e2be7c6c7ec..9689be8c133d 100644 --- a/x-pack/plugins/maps/public/locators.ts +++ b/x-pack/plugins/maps/public/locators.ts @@ -8,6 +8,7 @@ /* eslint-disable max-classes-per-file */ import rison from 'rison-node'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, @@ -17,13 +18,12 @@ import type { } from '../../../../src/plugins/data/public'; import { esFilters } from '../../../../src/plugins/data/public'; import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; import type { LocatorDefinition, LocatorPublic } from '../../../../src/plugins/share/public'; import type { LayerDescriptor } from '../common/descriptor_types'; import { INITIAL_LAYERS_KEY, APP_ID } from '../common/constants'; import { lazyLoadMapModules } from './lazy_load_bundle'; -export interface MapsAppLocatorParams extends SerializableState { +export interface MapsAppLocatorParams extends SerializableRecord { /** * If given, it will load the given map else will load the create a new map page. */ @@ -37,12 +37,12 @@ export interface MapsAppLocatorParams extends SerializableState { /** * Optionally set the initial Layers. */ - initialLayers?: LayerDescriptor[] & SerializableState; + initialLayers?: LayerDescriptor[] & SerializableRecord; /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the @@ -101,7 +101,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition string; }).encode_array(initialLayers); path = `${path}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; @@ -115,7 +115,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition { const location = await locator.getLocation({ filters: getData().query.filterManager.getFilters(), query: getData().query.queryString.getQuery(), - initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableRecord, timeRange: getData().query.timefilter.timefilter.getTime(), }); diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable_migrations.ts index 4bf39dc1f999..2a53198d8d24 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable_migrations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { moveAttribution } from '../common/migrations/move_attribution'; @@ -17,10 +17,10 @@ import { moveAttribution } from '../common/migrations/move_attribution'; * This is the embeddable migration registry. */ export const embeddableMigrations = { - '7.14.0': (state: SerializableState) => { + '7.14.0': (state: SerializableRecord) => { return { ...state, attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), - } as SerializableState; + } as SerializableRecord; }, }; diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 1119be32a04d..ecf89c8774c7 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -83,18 +83,6 @@ export class MapsPlugin implements Plugin { }, ]); - home.sampleData.replacePanelInSampleDatasetDashboard({ - sampleDataId: 'ecommerce', - dashboardId: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', - oldEmbeddableId: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - embeddableId: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', - // @ts-ignore - embeddableType: 'map', - embeddableConfig: { - isLayerTOCOpen: false, - }, - }); - home.sampleData.addSavedObjectsToSampleDataset('flights', getFlightsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('flights', [ diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 1b74b7ee7566..5245b374b9fe 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/metrics_entities/tsconfig.json b/x-pack/plugins/metrics_entities/tsconfig.json index 15e6aa160162..402b327a2dbf 100644 --- a/x-pack/plugins/metrics_entities/tsconfig.json +++ b/x-pack/plugins/metrics_entities/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index ec2a244c7546..0a43b15f412f 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -143,3 +143,31 @@ export interface AnalyticsMapReturnType { details: Record; // transform, job, or index details error: null | any; } + +export interface FeatureProcessor { + frequency_encoding: { + feature_name: string; + field: string; + frequency_map: Record; + }; + multi_encoding: { + processors: any[]; + }; + n_gram_encoding: { + feature_prefix?: string; + field: string; + length?: number; + n_grams: number[]; + start?: number; + }; + one_hot_encoding: { + field: string; + hot_map: string; + }; + target_mean_encoding: { + default_value: number; + feature_name: string; + field: string; + target_map: Record; + }; +} diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index 433deac02bc9..b3d36283b5d5 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { buildEsQuery, DslQuery } from '@kbn/es-query'; import { isPopulatedObject } from '../util/object_utils'; diff --git a/x-pack/plugins/ml/common/types/filters.ts b/x-pack/plugins/ml/common/types/filters.ts new file mode 100644 index 000000000000..200a659a9850 --- /dev/null +++ b/x-pack/plugins/ml/common/types/filters.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Filter { + filter_id: string; + description?: string; + items: string[]; +} + +interface FilterUsage { + jobs: string[]; + detectors: string[]; +} + +export interface FilterStats { + filter_id: string; + description?: string; + item_count: number; + used_by?: FilterUsage; +} diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 9d294e1323f7..bfb953777d85 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { LocatorPublic } from 'src/plugins/share/public'; import type { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; import type { JobId } from './anomaly_detection_jobs/job'; @@ -253,6 +253,6 @@ export type MlLocatorState = | FilterEditUrlState | MlGenericUrlState; -export type MlLocatorParams = MlLocatorState & SerializableState; +export type MlLocatorParams = MlLocatorState & SerializableRecord; export type MlLocator = LocatorPublic; diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx new file mode 100644 index 000000000000..f69df755d2aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx @@ -0,0 +1,187 @@ +/* + * 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, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiCallOut, EuiText, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import type { JobDependencies } from './jobs_export_service'; + +interface Props { + jobs: JobDependencies; +} + +export const ExportJobDependenciesWarningCallout: FC = ({ jobs: allJobs }) => { + const [jobs, jobsWithCalendars, jobsWithFilters] = filterJobs(allJobs); + const usingCalendars = jobsWithCalendars.length > 0; + const usingFilters = jobsWithFilters.length > 0; + + if (usingCalendars === false && usingFilters === false) { + return null; + } + + return ( + <> + + + + + {usingCalendars && ( + + } + > + + + )} + + {usingFilters && ( + + } + > + + + )} + + + + + ); +}; + +const CalendarJobList: FC<{ jobs: JobDependencies }> = ({ jobs }) => ( + <> + {jobs.length > 0 && ( + <> + {jobs.map(({ jobId, calendarIds }) => ( + <> + +
{jobId}
+ {calendarIds.length > 0 && ( + + )} +
+ + + ))} + + )} + +); + +const FilterJobList: FC<{ jobs: JobDependencies }> = ({ jobs }) => ( + <> + {jobs.length > 0 && ( + <> + {jobs.map(({ jobId, filterIds }) => ( + <> + +
{jobId}
+ {filterIds.length > 0 && ( + + )} +
+ + + ))} + + )} + +); + +function getTitle(jobs: JobDependencies, calendarCount: number, filterCount: number) { + if (calendarCount > 0 && filterCount === 0) { + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.calendarOnlyTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} {calendarCount, plural, one {a calendar} other {calendars}}', + values: { jobCount: jobs.length, calendarCount }, + } + ); + } + + if (calendarCount === 0 && filterCount > 0) { + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.filterOnlyTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} {filterCount, plural, one {a filter list} other {filter lists}}', + values: { jobCount: jobs.length, filterCount }, + } + ); + } + + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.filterAndCalendarTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} filter lists and calendars', + values: { jobCount: jobs.length }, + } + ); +} + +function filterJobs(jobs: JobDependencies) { + return jobs.reduce( + (acc, job) => { + const usingCalendars = job.calendarIds.length > 0; + const usingFilters = job.filterIds.length > 0; + if (usingCalendars || usingFilters) { + acc[0].push(job); + if (usingCalendars) { + acc[1].push(job); + } + if (usingFilters) { + acc[2].push(job); + } + } + return acc; + }, + [[], [], []] as JobDependencies[] + ); +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index 5ae6159f806b..ca1330738418 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -27,7 +27,9 @@ import { } from '@elastic/eui'; import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { ExportJobDependenciesWarningCallout } from './export_job_warning_callout'; import { JobsExportService } from './jobs_export_service'; +import type { JobDependencies } from './jobs_export_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import type { JobType } from '../../../../../common/types/saved_objects'; @@ -66,6 +68,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { [toasts] ); + const [jobDependencies, setJobDependencies] = useState([]); + const [selectedJobDependencies, setSelectedJobDependencies] = useState([]); + useEffect( function onFlyoutChange() { setLoadingADJobs(true); @@ -81,6 +86,22 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { .then(({ jobs }) => { setLoadingADJobs(false); setAdJobIds(jobs.map((j) => j.job_id)); + + jobsExportService + .getJobDependencies(jobs) + .then((jobDeps) => { + setJobDependencies(jobDeps); + setLoadingADJobs(false); + }) + .catch((error) => { + const errorTitle = i18n.translate( + 'xpack.ml.importExport.exportFlyout.calendarsError', + { + defaultMessage: 'Could not load calendars', + } + ); + displayErrorToast(error, errorTitle); + }); }) .catch((error) => { const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.adJobsError', { @@ -88,6 +109,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { }); displayErrorToast(error, errorTitle); }); + getDataFrameAnalytics() .then(({ data_frame_analytics: dataFrameAnalytics }) => { setLoadingDFAJobs(false); @@ -159,6 +181,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { switchTab(); }, [selectedJobIds]); + useEffect(() => { + setSelectedJobDependencies( + jobDependencies.filter(({ jobId }) => selectedJobIds.includes(jobId)) + ); + }, [selectedJobIds]); + function switchTab() { const jobType = selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; @@ -195,6 +223,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { + ; +export type FiltersPerJob = Array<{ jobId: string; filterIds: string[] }>; type ExportableConfigs = | Array< @@ -51,4 +55,82 @@ export class JobsExportService { (jobType === 'anomaly-detector' ? 'anomaly_detection' : 'data_frame_analytics') + '_jobs.json' ); } + + public async getJobDependencies(jobs: Job[]): Promise { + const calendars = await this._mlApiServices.calendars(); + + // create a map of all jobs in groups + const groups = jobs.reduce((acc, cur) => { + if (Array.isArray(cur.groups)) { + cur.groups.forEach((g) => { + if (acc[g] === undefined) { + acc[g] = []; + } + acc[g].push(cur.job_id); + }); + } + return acc; + }, {} as Record); + + const isGroup = (id: string) => groups[id] !== undefined; + + // create a map of all calendars in jobs + const calendarsPerJob = calendars.reduce((acc, cur) => { + cur.job_ids.forEach((jId) => { + if (jId === GLOBAL_CALENDAR) { + // add the calendar to all jobs + jobs.forEach((j) => { + if (acc[j.job_id] === undefined) { + acc[j.job_id] = []; + } + acc[j.job_id].push(cur.calendar_id); + }); + } else if (isGroup(jId)) { + // add the calendar to every job in this group + groups[jId].forEach((jId2) => { + if (acc[jId2] === undefined) { + acc[jId2] = []; + } + acc[jId2].push(cur.calendar_id); + }); + } else { + // add the calendar to just this job + if (acc[jId] === undefined) { + acc[jId] = []; + } + acc[jId].push(cur.calendar_id); + } + }); + return acc; + }, {} as Record); + + // create a map of all filters in jobs, + // by extracting the filters from the job's detectors + const filtersPerJob = jobs.reduce((acc, cur) => { + if (acc[cur.job_id] === undefined) { + acc[cur.job_id] = []; + } + cur.analysis_config.detectors.forEach((d) => { + if (d.custom_rules !== undefined) { + d.custom_rules.forEach((r) => { + if (r.scope !== undefined) { + Object.values(r.scope).forEach((scope) => { + acc[cur.job_id].push(scope.filter_id); + }); + } + }); + } + }); + return acc; + }, {} as Record); + + return jobs.map((j) => { + const jobId = j.job_id; + return { + jobId, + calendarIds: [...new Set(calendarsPerJob[jobId])] ?? [], + filterIds: [...new Set(filtersPerJob[jobId])] ?? [], + }; + }); + } } diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx index 7d573462a6c8..732be345a1ee 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -64,14 +64,23 @@ const SkippedJobList: FC<{ jobs: SkippedJobs[] }> = ({ jobs }) => ( <> {jobs.length > 0 && ( <> - {jobs.map(({ jobId, missingIndices }) => ( + {jobs.map(({ jobId, missingIndices, missingFilters }) => (
{jobId}
- + {missingIndices.length > 0 && ( + + )} + {missingFilters.length > 0 && ( + + )}
))} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 68f70d05ccc8..34e77d1b6492 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -47,6 +47,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { const { jobs: { bulkCreateJobs }, dataFrameAnalytics: { createDataFrameAnalytics }, + filters: { filters: getFilters }, } = useMlApiContext(); const { services: { @@ -130,7 +131,8 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { const validatedJobs = await jobImportService.validateJobs( loadedFile.jobs, loadedFile.jobType, - getIndexPatternTitles + getIndexPatternTitles, + getFilters ); if (loadedFile.jobType === 'anomaly-detector') { diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts index 85028426fa23..f767ac2f65cc 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts @@ -7,6 +7,7 @@ import type { JobType } from '../../../../../common/types/saved_objects'; import type { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; +import type { Filter } from '../../../../../common/types/filters'; import type { DataFrameAnalyticsConfig } from '../../../data_frame_analytics/common'; export interface ImportedAdJob { @@ -33,6 +34,7 @@ export interface JobIdObject { export interface SkippedJobs { jobId: string; missingIndices: string[]; + missingFilters: string[]; } function isImportedAdJobs(obj: any): obj is ImportedAdJob[] { @@ -127,17 +129,25 @@ export class JobImportService { public async validateJobs( jobs: ImportedAdJob[] | DataFrameAnalyticsConfig[], type: JobType, - getIndexPatternTitles: (refresh?: boolean) => Promise + getIndexPatternTitles: (refresh?: boolean) => Promise, + getFilters: () => Promise ) { const existingIndexPatterns = new Set(await getIndexPatternTitles()); + const existingFilters = new Set((await getFilters()).map((f) => f.filter_id)); const tempJobs: Array<{ jobId: string; destIndex?: string }> = []; - const tempSkippedJobIds: SkippedJobs[] = []; - - const commonJobs: Array<{ jobId: string; indices: string[]; destIndex?: string }> = + const skippedJobs: SkippedJobs[] = []; + + const commonJobs: Array<{ + jobId: string; + indices: string[]; + filters?: string[]; + destIndex?: string; + }> = type === 'anomaly-detector' ? (jobs as ImportedAdJob[]).map((j) => ({ jobId: j.job.job_id, indices: j.datafeed.indices, + filters: getFilterIdsFromJob(j.job), })) : (jobs as DataFrameAnalyticsConfig[]).map((j) => ({ jobId: j.id, @@ -145,24 +155,46 @@ export class JobImportService { indices: Array.isArray(j.source.index) ? j.source.index : [j.source.index], })); - commonJobs.forEach(({ jobId, indices, destIndex }) => { + commonJobs.forEach(({ jobId, indices, filters = [], destIndex }) => { const missingIndices = indices.filter((i) => existingIndexPatterns.has(i) === false); - if (missingIndices.length === 0) { + const missingFilters = filters.filter((i) => existingFilters.has(i) === false); + + if (missingIndices.length === 0 && missingFilters.length === 0) { tempJobs.push({ jobId, ...(type === 'data-frame-analytics' ? { destIndex } : {}), }); } else { - tempSkippedJobIds.push({ + skippedJobs.push({ jobId, missingIndices, + missingFilters, }); } }); return { jobs: tempJobs, - skippedJobs: tempSkippedJobIds, + skippedJobs, }; } } + +function getFilterIdsFromJob(job: Job) { + const filters = new Set(); + job.analysis_config.detectors.forEach((d) => { + if (d.custom_rules === undefined) { + return; + } + d.custom_rules.forEach((r) => { + if (r.scope === undefined) { + return; + } + Object.values(r.scope).forEach((s) => { + filters.add(s.filter_id); + }); + }); + }); + + return [...filters]; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index bac6b0b9274f..c2c2563c5ba7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -65,6 +65,12 @@ export interface LoadExploreDataArg { searchQuery: SavedSearchQuery; } +export interface ClassificationMetricItem { + className: string; + accuracy?: number; + recall?: number; +} + export const SEARCH_SIZE = 1000; export const TRAINING_PERCENT_MIN = 1; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 746b02d93400..824538a56acd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -308,7 +308,12 @@ export const DetailsStepForm: FC = ({ values: { defaultValue: DEFAULT_RESULTS_FIELD }, })} checked={useResultsFieldDefault === true} - onChange={() => setUseResultsFieldDefault(!useResultsFieldDefault)} + onChange={() => { + if (!useResultsFieldDefault === true) { + setFormState({ resultsField: undefined }); + } + setUseResultsFieldDefault(!useResultsFieldDefault); + }} data-test-subj="mlAnalyticsCreateJobWizardUseResultsFieldDefault" /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss index 73ced778821c..c429daaf3c8d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -41,3 +41,7 @@ $labelColumnWidth: 80px; text-transform: none; } } + +.mlDataFrameAnalyticsClassification__evaluationMetrics { + width: 60%; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 086adcecd077..fb103886635a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -34,6 +34,7 @@ import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; import { EvaluateStat } from './evaluate_stat'; +import { EvaluationQualityMetricsTable } from './evaluation_quality_metrics_table'; import { getRocCurveChartVegaLiteSpec } from './get_roc_curve_chart_vega_lite_spec'; @@ -85,6 +86,13 @@ const trainingDatasetHelpText = i18n.translate( } ); +const evaluationQualityMetricsHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.evaluationQualityMetricsHelpText', + { + defaultMessage: 'Evaluation quality metrics', + } +); + function getHelpText(dataSubsetTitle: string): string { let helpText = entireDatasetHelpText; if (dataSubsetTitle === SUBSET_TITLE.TESTING) { @@ -120,6 +128,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se error: errorConfusionMatrix, isLoading: isLoadingConfusionMatrix, overallAccuracy, + evaluationMetricsItems, } = useConfusionMatrix(jobConfig, searchQuery); useEffect(() => { @@ -365,46 +374,61 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se ) : null} {/* Accuracy and Recall */} - + + {evaluationQualityMetricsHelpText} + + + - + + + + + + + + - + {/* AUC ROC Chart */} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx new file mode 100644 index 000000000000..32820b69b8a8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiInMemoryTable, EuiPanel } from '@elastic/eui'; + +import { ClassificationMetricItem } from '../../../../common/analytics'; + +const columns = [ + { + field: 'className', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyClassColumn', + { + defaultMessage: 'Class', + } + ), + sortable: true, + truncateText: true, + }, + { + field: 'accuracy', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyAccuracyColumn', + { + defaultMessage: 'Accuracy', + } + ), + render: (value: number) => Math.round(value * 1000) / 1000, + }, + { + field: 'recall', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyRecallColumn', + { + defaultMessage: 'Recall', + } + ), + render: (value: number) => Math.round(value * 1000) / 1000, + }, +]; + +export const EvaluationQualityMetricsTable: FC<{ + evaluationMetricsItems: ClassificationMetricItem[]; +}> = ({ evaluationMetricsItems }) => ( + <> + + } + > + + + items={evaluationMetricsItems} + columns={columns} + pagination + sorting + /> + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index df48d2c5ab44..2a75acf823e8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -9,9 +9,11 @@ import { useState, useEffect } from 'react'; import { isClassificationEvaluateResponse, + ClassificationEvaluateResponse, ConfusionMatrix, ResultsSearchQuery, ANALYSIS_CONFIG_TYPE, + ClassificationMetricItem, } from '../../../../common/analytics'; import { isKeywordAndTextType } from '../../../../common/fields'; @@ -25,6 +27,37 @@ import { import { isTrainingFilter } from './is_training_filter'; +function getEvalutionMetricsItems(evalMetrics?: ClassificationEvaluateResponse['classification']) { + if (evalMetrics === undefined) return []; + + const accuracyMetrics = evalMetrics.accuracy?.classes || []; + const recallMetrics = evalMetrics.recall?.classes || []; + + const metricsMap = accuracyMetrics.reduce((acc, accuracyMetric) => { + acc[accuracyMetric.class_name] = { + className: accuracyMetric.class_name, + accuracy: accuracyMetric.value, + }; + return acc; + }, {} as Record); + + recallMetrics.forEach((recallMetric) => { + if (metricsMap[recallMetric.class_name] !== undefined) { + metricsMap[recallMetric.class_name] = { + recall: recallMetric.value, + ...metricsMap[recallMetric.class_name], + }; + } else { + metricsMap[recallMetric.class_name] = { + className: recallMetric.class_name, + recall: recallMetric.value, + }; + } + }); + + return Object.values(metricsMap); +} + export const useConfusionMatrix = ( jobConfig: DataFrameAnalyticsConfig, searchQuery: ResultsSearchQuery @@ -32,6 +65,9 @@ export const useConfusionMatrix = ( const [confusionMatrixData, setConfusionMatrixData] = useState([]); const [overallAccuracy, setOverallAccuracy] = useState(null); const [avgRecall, setAvgRecall] = useState(null); + const [evaluationMetricsItems, setEvaluationMetricsItems] = useState( + [] + ); const [isLoading, setIsLoading] = useState(false); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); @@ -81,6 +117,7 @@ export const useConfusionMatrix = ( setConfusionMatrixData(confusionMatrix || []); setAvgRecall(evalData.eval?.classification?.recall?.avg_recall || null); setOverallAccuracy(evalData.eval?.classification?.accuracy?.overall_accuracy || null); + setEvaluationMetricsItems(getEvalutionMetricsItems(evalData.eval?.classification)); setIsLoading(false); } else { setIsLoading(false); @@ -98,5 +135,13 @@ export const useConfusionMatrix = ( loadConfusionMatrixData(); }, [JSON.stringify([jobConfig, searchQuery])]); - return { avgRecall, confusionMatrixData, docsCount, error, isLoading, overallAccuracy }; + return { + avgRecall, + confusionMatrixData, + docsCount, + error, + isLoading, + overallAccuracy, + evaluationMetricsItems, + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index c661c40958bc..86734c87c5e1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -133,6 +133,9 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo optional: true, formKey: 'etaGrowthRatePerTree', }, + feature_processors: { + optional: true, + }, max_optimization_rounds_per_hyperparameter: { optional: true, formKey: 'maxOptimizationRoundsPerHyperparameter', @@ -235,6 +238,9 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo loss_function_parameter: { optional: true, }, + feature_processors: { + optional: true, + }, early_stopping_enabled: { optional: true, ignore: true, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 5065aefd921d..71da56c9e14e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -108,6 +108,28 @@ const getSourceIndexString = (state: State) => { return ''; }; +const isSourceIndexNameValid = ( + sourceIndexName: string, + sourceIndex: string | Array | undefined +) => { + // general check against Kibana index pattern names, but since this is about the advanced editor + // with support for arrays in the job config, we also need to check that each individual name + // doesn't include a comma if index names are supplied as an array. + // `indexPatterns.validate()` returns a map of messages, we're only interested here if it's valid or not. + // If there are no messages, it means the index pattern is valid. + let sourceIndexNameValid = Object.keys(indexPatterns.validate(sourceIndexName)).length === 0; + if (sourceIndexNameValid) { + if (typeof sourceIndex === 'string') { + sourceIndexNameValid = !sourceIndex.includes(','); + } + if (Array.isArray(sourceIndex)) { + sourceIndexNameValid = !sourceIndex.some((d) => d?.includes(',')); + } + } + + return sourceIndexNameValid; +}; + /** * Validates num_top_feature_importance_values. Must be an integer >= 0. */ @@ -129,21 +151,8 @@ export const validateAdvancedEditor = (state: State): State => { const sourceIndexName = getSourceIndexString(state); const sourceIndexNameEmpty = sourceIndexName === ''; - // general check against Kibana index pattern names, but since this is about the advanced editor - // with support for arrays in the job config, we also need to check that each individual name - // doesn't include a comma if index names are supplied as an array. - // `indexPatterns.validate()` returns a map of messages, we're only interested here if it's valid or not. - // If there are no messages, it means the index pattern is valid. - let sourceIndexNameValid = Object.keys(indexPatterns.validate(sourceIndexName)).length === 0; const sourceIndex = jobConfig?.source?.index; - if (sourceIndexNameValid) { - if (typeof sourceIndex === 'string') { - sourceIndexNameValid = !sourceIndex.includes(','); - } - if (Array.isArray(sourceIndex)) { - sourceIndexNameValid = !sourceIndex.some((d) => d?.includes(',')); - } - } + const sourceIndexNameValid = isSourceIndexNameValid(sourceIndexName, sourceIndex); const destinationIndexName = jobConfig?.dest?.index ?? ''; const destinationIndexNameEmpty = destinationIndexName === ''; @@ -562,9 +571,8 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_FORM: const { jobConfig: config } = state; const { jobId } = state.form; - // Persist form state when switching back from advanced editor // @ts-ignore - const formState = { ...state.form, ...getFormStateFromJobConfig(config, false) }; + const formState = getFormStateFromJobConfig(config, false); if (typeof jobId === 'string' && jobId.trim() !== '') { formState.jobId = jobId; @@ -585,6 +593,11 @@ export function reducer(state: State, action: Action): State { ); } + const sourceIndexName = getSourceIndexString(state); + const sourceIndex = config?.source?.index; + const sourceIndexNameValid = isSourceIndexNameValid(sourceIndexName, sourceIndex); + formState.sourceIndexNameValid = sourceIndexNameValid; + return validateForm({ ...state, // @ts-ignore diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 22efe6f9eb3e..3c9e39419073 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -17,6 +17,7 @@ import { DataFrameAnalyticsConfig, DataFrameAnalyticsId, DataFrameAnalysisConfigType, + FeatureProcessor, } from '../../../../../../../common/types/data_frame_analytics'; import { isClassificationAnalysis } from '../../../../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; @@ -61,12 +62,13 @@ export interface State { destinationIndexNameEmpty: boolean; destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; - earlyStoppingEnabled: undefined | boolean; downsampleFactor: undefined | number; + earlyStoppingEnabled: undefined | boolean; eta: undefined | number; etaGrowthRatePerTree: undefined | number; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; + featureProcessors: undefined | FeatureProcessor[]; gamma: undefined | number; includes: string[]; jobId: DataFrameAnalyticsId; @@ -78,6 +80,8 @@ export interface State { jobConfigQuery: any; jobConfigQueryString: string | undefined; lambda: number | undefined; + lossFunction: string | undefined; + lossFunctionParameter: number | undefined; loadingFieldOptions: boolean; maxNumThreads: undefined | number; maxOptimizationRoundsPerHyperparameter: undefined | number; @@ -147,6 +151,7 @@ export const getInitialState = (): State => ({ etaGrowthRatePerTree: undefined, featureBagFraction: undefined, featureInfluenceThreshold: undefined, + featureProcessors: undefined, gamma: undefined, includes: [], jobId: '', @@ -158,6 +163,8 @@ export const getInitialState = (): State => ({ jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, + lossFunction: undefined, + lossFunctionParameter: undefined, loadingFieldOptions: false, maxNumThreads: DEFAULT_MAX_NUM_THREADS, maxOptimizationRoundsPerHyperparameter: undefined, @@ -268,8 +275,15 @@ export const getJobConfigFromFormState = ( formState.featureBagFraction && { feature_bag_fraction: formState.featureBagFraction, }, + formState.featureProcessors && { + feature_processors: formState.featureProcessors, + }, formState.gamma && { gamma: formState.gamma }, formState.lambda && { lambda: formState.lambda }, + formState.lossFunction && { loss_function: formState.lossFunction }, + formState.lossFunctionParameter && { + loss_function_parameter: formState.lossFunctionParameter, + }, formState.maxOptimizationRoundsPerHyperparameter && { max_optimization_rounds_per_hyperparameter: formState.maxOptimizationRoundsPerHyperparameter, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts index 0cb02cf68226..cf49031c665e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts @@ -11,18 +11,19 @@ import { http } from '../http_service'; import { basePath } from './index'; +import type { Filter, FilterStats } from '../../../../common/types/filters'; export const filters = { filters(obj?: { filterId?: string }) { const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; - return http({ + return http({ path: `${basePath()}/filters${filterId}`, method: 'GET', }); }, filtersStats() { - return http({ + return http({ path: `${basePath()}/filters/_stats`, method: 'GET', }); @@ -34,7 +35,7 @@ export const filters = { description, items, }); - return http({ + return http({ path: `${basePath()}/filters`, method: 'PUT', body, @@ -48,7 +49,7 @@ export const filters = { ...(removeItems !== undefined ? { removeItems } : {}), }); - return http({ + return http({ path: `${basePath()}/filters/${filterId}`, method: 'PUT', body, @@ -56,7 +57,7 @@ export const filters = { }, deleteFilter(filterId: string) { - return http({ + return http<{ acknowledged: boolean }>({ path: `${basePath()}/filters/${filterId}`, method: 'DELETE', }); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index ee84fb222bb5..35fc14e23624 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 7bbbc81b6de7..0fe4b7805d07 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -19,7 +19,8 @@ "source.ip", "winlog.event_data.LogonType", "user.name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 4b7094e92c6e..cde52bf7d33c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index aaee46d9cf80..2360233937c2 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -20,7 +20,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index bc08aa21f327..2a3b4b010018 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.port" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index d709eb21d7c6..792d7f251398 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json index 9ee26b314c64..d2ecf4df624f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json @@ -19,7 +19,8 @@ "host.name", "user.name", "source.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json index 98d145a91d9a..b4294227c49d 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json @@ -18,7 +18,8 @@ "aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json index 0332fd53814a..ad2627ff4f21 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json @@ -34,7 +34,8 @@ "destination.ip", "host.name", "dns.question.etld_plus_one" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json index 613a446750e5..6fff7246a249 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json @@ -19,7 +19,8 @@ "host.name", "user.name", "winlog.event_data.Path" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index f48b71dedf3e..a2b71ae57217 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -9,14 +9,8 @@ import { estypes } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import type { MlClient } from '../../lib/ml_client'; -// import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; - -export interface Filter { - filter_id: string; - description?: string; - items: string[]; -} +import type { Job } from '../../../common/types/anomaly_detection_jobs'; +import type { Filter, FilterStats } from '../../../common/types/filters'; export interface FormFilter { filterId: string; @@ -43,13 +37,6 @@ interface FilterUsage { detectors: string[]; } -interface FilterStats { - filter_id: string; - description?: string; - item_count: number; - used_by: FilterUsage; -} - interface FiltersInUse { [id: string]: FilterUsage; } diff --git a/x-pack/plugins/ml/server/models/filter/index.ts b/x-pack/plugins/ml/server/models/filter/index.ts index ec6f922ada2f..d5663478b481 100644 --- a/x-pack/plugins/ml/server/models/filter/index.ts +++ b/x-pack/plugins/ml/server/models/filter/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { FilterManager, Filter, FormFilter, UpdateFilter } from './filter_manager'; +export { FilterManager, FormFilter, UpdateFilter } from './filter_manager'; diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 346bf510c6c0..646a250b9668 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -22,8 +22,6 @@ "ValidateDataFrameAnalytics", "DataVisualizer", - "GetOverallStats", - "GetStatsForFields", "GetHistogramsForFields", "AnomalyDetectors", @@ -34,6 +32,7 @@ "GetAnomalyDetectorsStats", "GetAnomalyDetectorsStatsById", "CloseAnomalyDetectorsJob", + "ResetAnomalyDetectorsJob", "ValidateAnomalyDetector", "ForecastAnomalyDetector", "GetRecords", @@ -73,6 +72,7 @@ "ForceStartDatafeeds", "StopDatafeeds", "CloseJobs", + "ResetJobs", "JobsSummary", "JobsWithTimeRange", "GetJobForCloning", diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index 6d01a853698b..4748911105e8 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "target": "es6", - "moduleResolution": "node" + "outDir": "./target/types" }, "include": [ "schema_worker.ts", diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 8e859c35e3f8..db8fc463b055 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 8be0ca23d4d6..3f03680e687b 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -220,29 +220,29 @@ export const CLUSTER_DETAILS_FETCH_INTERVAL = 10800000; export const USAGE_FETCH_INTERVAL = 1200000; /** - * The prefix for all alert types used by monitoring - */ -export const ALERT_PREFIX = 'monitoring_'; -export const ALERT_LICENSE_EXPIRATION = `${ALERT_PREFIX}alert_license_expiration`; -export const ALERT_CLUSTER_HEALTH = `${ALERT_PREFIX}alert_cluster_health`; -export const ALERT_CPU_USAGE = `${ALERT_PREFIX}alert_cpu_usage`; -export const ALERT_DISK_USAGE = `${ALERT_PREFIX}alert_disk_usage`; -export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`; -export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`; -export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`; -export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; -export const ALERT_MEMORY_USAGE = `${ALERT_PREFIX}alert_jvm_memory_usage`; -export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`; -export const ALERT_THREAD_POOL_SEARCH_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_search_rejections`; -export const ALERT_THREAD_POOL_WRITE_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_write_rejections`; -export const ALERT_CCR_READ_EXCEPTIONS = `${ALERT_PREFIX}ccr_read_exceptions`; -export const ALERT_LARGE_SHARD_SIZE = `${ALERT_PREFIX}shard_size`; + * The prefix for all rule types used by monitoring + */ +export const RULE_PREFIX = 'monitoring_'; +export const RULE_LICENSE_EXPIRATION = `${RULE_PREFIX}alert_license_expiration`; +export const RULE_CLUSTER_HEALTH = `${RULE_PREFIX}alert_cluster_health`; +export const RULE_CPU_USAGE = `${RULE_PREFIX}alert_cpu_usage`; +export const RULE_DISK_USAGE = `${RULE_PREFIX}alert_disk_usage`; +export const RULE_NODES_CHANGED = `${RULE_PREFIX}alert_nodes_changed`; +export const RULE_ELASTICSEARCH_VERSION_MISMATCH = `${RULE_PREFIX}alert_elasticsearch_version_mismatch`; +export const RULE_KIBANA_VERSION_MISMATCH = `${RULE_PREFIX}alert_kibana_version_mismatch`; +export const RULE_LOGSTASH_VERSION_MISMATCH = `${RULE_PREFIX}alert_logstash_version_mismatch`; +export const RULE_MEMORY_USAGE = `${RULE_PREFIX}alert_jvm_memory_usage`; +export const RULE_MISSING_MONITORING_DATA = `${RULE_PREFIX}alert_missing_monitoring_data`; +export const RULE_THREAD_POOL_SEARCH_REJECTIONS = `${RULE_PREFIX}alert_thread_pool_search_rejections`; +export const RULE_THREAD_POOL_WRITE_REJECTIONS = `${RULE_PREFIX}alert_thread_pool_write_rejections`; +export const RULE_CCR_READ_EXCEPTIONS = `${RULE_PREFIX}ccr_read_exceptions`; +export const RULE_LARGE_SHARD_SIZE = `${RULE_PREFIX}shard_size`; /** - * Legacy alerts details/label for server and public use + * Legacy rules details/label for server and public use */ -export const LEGACY_ALERT_DETAILS = { - [ALERT_CLUSTER_HEALTH]: { +export const LEGACY_RULE_DETAILS = { + [RULE_CLUSTER_HEALTH]: { label: i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { defaultMessage: 'Cluster health', }), @@ -250,7 +250,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the health of the cluster changes.', }), }, - [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: { + [RULE_ELASTICSEARCH_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { defaultMessage: 'Elasticsearch version mismatch', }), @@ -261,7 +261,7 @@ export const LEGACY_ALERT_DETAILS = { } ), }, - [ALERT_KIBANA_VERSION_MISMATCH]: { + [RULE_KIBANA_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { defaultMessage: 'Kibana version mismatch', }), @@ -269,7 +269,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluser has multiple versions of Kibana.', }), }, - [ALERT_LICENSE_EXPIRATION]: { + [RULE_LICENSE_EXPIRATION]: { label: i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { defaultMessage: 'License expiration', }), @@ -277,7 +277,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluster license is about to expire.', }), }, - [ALERT_LOGSTASH_VERSION_MISMATCH]: { + [RULE_LOGSTASH_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { defaultMessage: 'Logstash version mismatch', }), @@ -285,7 +285,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluster has multiple versions of Logstash.', }), }, - [ALERT_NODES_CHANGED]: { + [RULE_NODES_CHANGED]: { label: i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { defaultMessage: 'Nodes changed', }), @@ -296,10 +296,10 @@ export const LEGACY_ALERT_DETAILS = { }; /** - * Alerts details/label for server and public use + * Rules details/label for server and public use */ -export const ALERT_DETAILS = { - [ALERT_CPU_USAGE]: { +export const RULE_DETAILS = { + [RULE_CPU_USAGE]: { label: i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { defaultMessage: 'CPU Usage', }), @@ -321,7 +321,7 @@ export const ALERT_DETAILS = { } as CommonAlertParamDetail, }, }, - [ALERT_DISK_USAGE]: { + [RULE_DISK_USAGE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', { @@ -343,7 +343,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when the disk usage for a node is consistently high.', }), }, - [ALERT_MEMORY_USAGE]: { + [RULE_MEMORY_USAGE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', { @@ -365,7 +365,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when a node reports high memory usage.', }), }, - [ALERT_MISSING_MONITORING_DATA]: { + [RULE_MISSING_MONITORING_DATA]: { paramDetails: { duration: { label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { @@ -387,7 +387,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when monitoring data is missing.', }), }, - [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: { + [RULE_THREAD_POOL_SEARCH_REJECTIONS]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { @@ -412,7 +412,7 @@ export const ALERT_DETAILS = { 'Alert when the number of rejections in the search thread pool exceeds the threshold.', }), }, - [ALERT_THREAD_POOL_WRITE_REJECTIONS]: { + [RULE_THREAD_POOL_WRITE_REJECTIONS]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { @@ -437,7 +437,7 @@ export const ALERT_DETAILS = { 'Alert when the number of rejections in the write thread pool exceeds the threshold.', }), }, - [ALERT_CCR_READ_EXCEPTIONS]: { + [RULE_CCR_READ_EXCEPTIONS]: { paramDetails: { duration: { label: i18n.translate( @@ -456,7 +456,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert if any CCR read exceptions have been detected.', }), }, - [ALERT_LARGE_SHARD_SIZE]: { + [RULE_LARGE_SHARD_SIZE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.shardSize.paramDetails.threshold.label', { @@ -482,74 +482,74 @@ export const ALERT_DETAILS = { }, }; -export const ALERT_PANEL_MENU = [ +export const RULE_PANEL_MENU = [ { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.clusterHealth', { defaultMessage: 'Cluster health', }), - alerts: [ - { alertName: ALERT_NODES_CHANGED }, - { alertName: ALERT_CLUSTER_HEALTH }, - { alertName: ALERT_ELASTICSEARCH_VERSION_MISMATCH }, - { alertName: ALERT_KIBANA_VERSION_MISMATCH }, - { alertName: ALERT_LOGSTASH_VERSION_MISMATCH }, + rules: [ + { ruleName: RULE_NODES_CHANGED }, + { ruleName: RULE_CLUSTER_HEALTH }, + { ruleName: RULE_ELASTICSEARCH_VERSION_MISMATCH }, + { ruleName: RULE_KIBANA_VERSION_MISMATCH }, + { ruleName: RULE_LOGSTASH_VERSION_MISMATCH }, ], }, { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.resourceUtilization', { defaultMessage: 'Resource utilization', }), - alerts: [ - { alertName: ALERT_CPU_USAGE }, - { alertName: ALERT_DISK_USAGE }, - { alertName: ALERT_MEMORY_USAGE }, - { alertName: ALERT_LARGE_SHARD_SIZE }, + rules: [ + { ruleName: RULE_CPU_USAGE }, + { ruleName: RULE_DISK_USAGE }, + { ruleName: RULE_MEMORY_USAGE }, + { ruleName: RULE_LARGE_SHARD_SIZE }, ], }, { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.errors', { defaultMessage: 'Errors and exceptions', }), - alerts: [ - { alertName: ALERT_MISSING_MONITORING_DATA }, - { alertName: ALERT_LICENSE_EXPIRATION }, - { alertName: ALERT_THREAD_POOL_SEARCH_REJECTIONS }, - { alertName: ALERT_THREAD_POOL_WRITE_REJECTIONS }, - { alertName: ALERT_CCR_READ_EXCEPTIONS }, + rules: [ + { ruleName: RULE_MISSING_MONITORING_DATA }, + { ruleName: RULE_LICENSE_EXPIRATION }, + { ruleName: RULE_THREAD_POOL_SEARCH_REJECTIONS }, + { ruleName: RULE_THREAD_POOL_WRITE_REJECTIONS }, + { ruleName: RULE_CCR_READ_EXCEPTIONS }, ], }, ]; /** - * A listing of all alert types - */ -export const ALERTS = [ - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + * A listing of all rule types + */ +export const RULES = [ + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, ]; /** - * A list of all legacy alerts, which means they are powered by watcher - */ -export const LEGACY_ALERTS = [ - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_LOGSTASH_VERSION_MISMATCH, + * A list of all legacy rules, which means they are powered by watcher + */ +export const LEGACY_RULES = [ + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, ]; /** @@ -564,9 +564,9 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email'; export const ALERT_ACTION_TYPE_LOG = '.server-log'; /** - * To enable modifing of alerts in under actions + * To enable modifing of rules in under actions */ -export const ALERT_REQUIRES_APP_CONTEXT = false; +export const RULE_REQUIRES_APP_CONTEXT = false; export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 39323e0d7b27..17bbffce19a1 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -20,7 +20,7 @@ export interface RulesByType { } export interface CommonAlertStatus { states: CommonAlertState[]; - rawAlert: Alert | SanitizedAlert; + sanitizedRule: Alert | SanitizedAlert; } export interface CommonAlertState { diff --git a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx index df0cbb43f856..8ea073d6132d 100644 --- a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx @@ -15,15 +15,19 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../legacy_shims'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { MonitoringStartPluginDependencies } from '../types'; export const AlertsDropdown: React.FC<{}> = () => { const $injector = Legacy.shims.getAngularInjector(); const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); + const { navigateToApp } = useKibana< + MonitoringStartPluginDependencies['core'] + >().services.application; const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopover = () => { - alertsEnableModalProvider.enableAlerts(); setIsPopoverOpen(false); }; @@ -32,6 +36,7 @@ export const AlertsDropdown: React.FC<{}> = () => { }; const createDefaultRules = () => { + alertsEnableModalProvider.enableAlerts(); closePopover(); }; @@ -44,13 +49,21 @@ export const AlertsDropdown: React.FC<{}> = () => { ); - const items = [ + const items: EuiContextMenuPanelDescriptor['items'] = [ { name: i18n.translate('xpack.monitoring.alerts.dropdown.createAlerts', { defaultMessage: 'Create default rules', }), onClick: createDefaultRules, }, + { + name: i18n.translate('xpack.monitoring.alerts.dropdown.manageRules', { + defaultMessage: 'Manage rules', + }), + icon: 'tableOfContents', + onClick: () => + navigateToApp('management', { path: '/insightsAndAlerting/triggersActions/rules' }), + }, ]; const panels: EuiContextMenuPanelDescriptor[] = [ diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 4f6572d25b05..6b1c8c508556 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -46,7 +46,7 @@ export const AlertsBadge: React.FC = (props: Props) => { // We do not always have the alerts that each consumer wants due to licensing const { stateFilter = () => true } = props; const alertsList = Object.values(props.alerts).flat(); - const alerts = alertsList.filter((alertItem) => Boolean(alertItem?.rawAlert)); + const alerts = alertsList.filter((alertItem) => Boolean(alertItem?.sanitizedRule)); const [showPopover, setShowPopover] = React.useState(null); const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); const alertCount = inSetupMode diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index eaf45856864c..b4e14ca0b0ac 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -103,7 +103,7 @@ export const AlertsCallout: React.FC = (props: Props) => { } )} } + label={} /> diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index b660bdcfdce8..1de9a175026a 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - ALERT_CCR_READ_EXCEPTIONS, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_CCR_READ_EXCEPTIONS, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; @@ -38,17 +38,17 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { export function createCCRReadExceptionsAlertType(): AlertTypeModel { return { - id: ALERT_CCR_READ_EXCEPTIONS, - description: ALERT_DETAILS[ALERT_CCR_READ_EXCEPTIONS].description, + id: RULE_CCR_READ_EXCEPTIONS, + description: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCCRReadExceptions}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index cad2cbdf613a..ec7a583ec2ba 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -8,27 +8,23 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { - ALERT_CPU_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, -} from '../../../common/constants'; +import { RULE_CPU_USAGE, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; import { Expression, Props } from '../components/param_details_form/expression'; export function createCpuUsageAlertType(): AlertTypeModel { return { - id: ALERT_CPU_USAGE, - description: ALERT_DETAILS[ALERT_CPU_USAGE].description, + id: RULE_CPU_USAGE, + description: RULE_DETAILS[RULE_CPU_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCpuThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 2cacb9d3a3b7..779945da0c9e 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -12,24 +12,24 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - ALERT_DISK_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_DISK_USAGE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createDiskUsageAlertType(): AlertTypeModel { return { - id: ALERT_DISK_USAGE, - description: ALERT_DETAILS[ALERT_DISK_USAGE].description, + id: RULE_DISK_USAGE, + description: RULE_DETAILS[RULE_DISK_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaDiskThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx index 89139f12dacc..e0f595abe760 100644 --- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - ALERT_LARGE_SHARD_SIZE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_LARGE_SHARD_SIZE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; @@ -38,17 +38,17 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { export function createLargeShardSizeAlertType(): AlertTypeModel { return { - id: ALERT_LARGE_SHARD_SIZE, - description: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].description, + id: RULE_LARGE_SHARD_SIZE, + description: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index 82bdf2786f89..cac4873bc0c7 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -11,16 +11,16 @@ import { EuiTextColor, EuiSpacer } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - LEGACY_ALERTS, - LEGACY_ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + LEGACY_RULES, + LEGACY_RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createLegacyAlertTypes(): AlertTypeModel[] { - return LEGACY_ALERTS.map((legacyAlert) => { + return LEGACY_RULES.map((legacyAlert) => { return { id: legacyAlert, - description: LEGACY_ALERT_DETAILS[legacyAlert].description, + description: LEGACY_RULE_DETAILS[legacyAlert].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; @@ -38,7 +38,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { ), defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; }); } diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx index 63ae489677c7..0ba35969d8d1 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx @@ -6,17 +6,17 @@ */ import { - ALERT_CPU_USAGE, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_THREAD_POOL_WRITE_REJECTIONS, + RULE_CPU_USAGE, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_THREAD_POOL_WRITE_REJECTIONS, } from '../../../common/constants'; import { AlertSeverity } from '../../../common/enums'; import { getAlertPanelsByCategory } from './get_alert_panels_by_category'; import { - ALERT_LICENSE_EXPIRATION, - ALERT_NODES_CHANGED, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_LICENSE_EXPIRATION, + RULE_NODES_CHANGED, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../common/constants'; import { AlertExecutionStatusValues } from '../../../../alerting/common'; import { AlertState } from '../../../common/types/alerts'; @@ -93,7 +93,7 @@ describe('getAlertPanelsByCategory', () => { return { states, - rawAlert: { + sanitizedRule: { alertTypeId: type, name: `${type}_label`, ...mockAlert, @@ -107,32 +107,32 @@ describe('getAlertPanelsByCategory', () => { describe('non setup mode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), ]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should not show any alert if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should allow for state filtering', () => { - const alerts = [getAlert(ALERT_CPU_USAGE, 2)]; + const alerts = [getAlert(RULE_CPU_USAGE, 2)]; const customStateFilter = (state: AlertState) => state.nodeName === 'es_name_0'; const result = getAlertPanelsByCategory(panelTitle, false, alerts, customStateFilter); expect(result).toMatchSnapshot(); @@ -142,25 +142,25 @@ describe('getAlertPanelsByCategory', () => { describe('setup mode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), ]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should still show alerts if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx index 26dbe25cdd5a..f455e75f688c 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { AlertPanel } from '../panel'; -import { ALERT_PANEL_MENU } from '../../../common/constants'; +import { RULE_PANEL_MENU } from '../../../common/constants'; import { getDateFromNow, getCalendar } from '../../../common/formatting'; import { AlertState, @@ -22,7 +22,7 @@ import { Legacy } from '../../legacy_shims'; interface MenuAlert { alert: CommonAlert; - alertName: string; + ruleName: string; states: CommonAlertState[]; } interface MenuItem { @@ -36,13 +36,13 @@ export function getAlertPanelsByCategory( alerts: CommonAlertStatus[], stateFilter: (state: AlertState) => boolean ) { - // return items organized by categories in ALERT_PANEL_MENU + // return items organized by categories in RULE_PANEL_MENU // only show rules in setup mode const menu = inSetupMode - ? ALERT_PANEL_MENU.reduce((acc, category) => { + ? RULE_PANEL_MENU.reduce((acc, category) => { // check if we have any rules with that match this category - const alertsInCategory = category.alerts.filter((alert) => - alerts.find(({ rawAlert }) => rawAlert.alertTypeId === alert.alertName) + const alertsInCategory = category.rules.filter((rule) => + alerts.find(({ sanitizedRule }) => sanitizedRule.alertTypeId === rule.ruleName) ); // return all the categories that have rules and the rules if (alertsInCategory.length > 0) { @@ -51,14 +51,14 @@ export function getAlertPanelsByCategory( ...category, // add the corresponding rules that belong to this category alerts: alertsInCategory - .map(({ alertName }) => { + .map(({ ruleName }) => { return alerts - .filter(({ rawAlert }) => rawAlert.alertTypeId === alertName) + .filter(({ sanitizedRule }) => sanitizedRule.alertTypeId === ruleName) .map((alert) => { return { - alert: alert.rawAlert, + alert: alert.sanitizedRule, states: [], - alertName, + ruleName, }; }); }) @@ -68,13 +68,14 @@ export function getAlertPanelsByCategory( } return acc; }, []) - : ALERT_PANEL_MENU.reduce((acc, category) => { - // return items organized by categories in ALERT_PANEL_MENU, then rule name, then the actual alerts + : RULE_PANEL_MENU.reduce((acc, category) => { + // return items organized by categories in RULE_PANEL_MENU, then rule name, then the actual alerts const firingAlertsInCategory: MenuAlert[] = []; let categoryFiringAlertCount = 0; - for (const { alertName } of category.alerts) { + for (const { ruleName } of category.rules) { const foundAlerts = alerts.filter( - ({ rawAlert, states }) => alertName === rawAlert.alertTypeId && states.length > 0 + ({ sanitizedRule, states }) => + ruleName === sanitizedRule.alertTypeId && states.length > 0 ); if (foundAlerts.length > 0) { foundAlerts.forEach((foundAlert) => { @@ -82,9 +83,9 @@ export function getAlertPanelsByCategory( const states = foundAlert.states.filter(({ state }) => stateFilter(state)); if (states.length > 0) { firingAlertsInCategory.push({ - alert: foundAlert.rawAlert, + alert: foundAlert.sanitizedRule, states, - alertName, + ruleName, }); categoryFiringAlertCount += states.length; } @@ -169,7 +170,7 @@ export function getAlertPanelsByCategory( panels.push({ id: nodeIndex + 1, title: `${category.label}`, - items: category.alerts.map(({ alert, alertName, states }) => { + items: category.alerts.map(({ alert, ruleName, states }) => { const filteredStates = states.filter(({ state }) => stateFilter(state)); const name = inSetupMode ? ( {alert.name} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx index 69179fb1d07d..5f9e8ba8b64f 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx @@ -6,17 +6,17 @@ */ import { - ALERT_CPU_USAGE, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_THREAD_POOL_WRITE_REJECTIONS, + RULE_CPU_USAGE, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_THREAD_POOL_WRITE_REJECTIONS, } from '../../../common/constants'; import { AlertSeverity } from '../../../common/enums'; import { getAlertPanelsByNode } from './get_alert_panels_by_node'; import { - ALERT_LICENSE_EXPIRATION, - ALERT_NODES_CHANGED, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_LICENSE_EXPIRATION, + RULE_NODES_CHANGED, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../common/constants'; import { AlertExecutionStatusValues } from '../../../../alerting/common'; import { AlertState } from '../../../common/types/alerts'; @@ -88,7 +88,7 @@ describe('getAlertPanelsByNode', () => { } return { - rawAlert: { + sanitizedRule: { id: `${type}_${firingCount}`, alertTypeId: type, name: `${type}_label`, @@ -103,17 +103,17 @@ describe('getAlertPanelsByNode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), { states: [ { firing: true, meta: {}, state: { cluster, ui, nodeId: 'es1', nodeName: 'es_name_1' } }, ], - rawAlert: { - id: `${ALERT_NODES_CHANGED}_3`, - alertTypeId: ALERT_NODES_CHANGED, - name: `${ALERT_NODES_CHANGED}_label_2`, + sanitizedRule: { + id: `${RULE_NODES_CHANGED}_3`, + alertTypeId: RULE_NODES_CHANGED, + name: `${RULE_NODES_CHANGED}_label_2`, ...mockAlert, }, }, @@ -123,16 +123,16 @@ describe('getAlertPanelsByNode', () => { }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should not show any alert if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); expect(result).toMatchSnapshot(); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx index 75eb03b9bad2..b8b4397c98b2 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx @@ -37,8 +37,8 @@ export function getAlertPanelsByNode( [uuid: string]: CommonAlertState[]; } = {}; - for (const { states, rawAlert } of alerts) { - const { id: alertId } = rawAlert; + for (const { states, sanitizedRule } of alerts) { + const { id: alertId } = sanitizedRule; for (const alertState of states.filter(({ state: _state }) => stateFilter(_state))) { const { state } = alertState; statesByNodes[state.nodeId] = statesByNodes[state.nodeId] || []; @@ -46,7 +46,7 @@ export function getAlertPanelsByNode( alertsByNodes[state.nodeId] = alertsByNodes[state.nodeId] || {}; alertsByNodes[state.nodeId][alertId] = alertsByNodes[alertState.state.nodeId][alertId] || { - alert: rawAlert, + alert: sanitizedRule, states: [], count: 0, }; diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 6639b4f2f6a4..3e55b6d5454f 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -12,24 +12,24 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - ALERT_MEMORY_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_MEMORY_USAGE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createMemoryUsageAlertType(): AlertTypeModel { return { - id: ALERT_MEMORY_USAGE, - description: ALERT_DETAILS[ALERT_MEMORY_USAGE].description, + id: RULE_MEMORY_USAGE, + description: RULE_DETAILS[RULE_MEMORY_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaJvmThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx index 4c833c93e070..4c90f067d47c 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -10,16 +10,16 @@ import React from 'react'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { validate } from './validation'; import { - ALERT_MISSING_MONITORING_DATA, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_MISSING_MONITORING_DATA, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { Expression } from './expression'; export function createMissingMonitoringDataAlertType(): AlertTypeModel { return { - id: ALERT_MISSING_MONITORING_DATA, - description: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].description, + id: RULE_MISSING_MONITORING_DATA, + description: RULE_DETAILS[RULE_MISSING_MONITORING_DATA].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaMissingData}`; @@ -27,11 +27,11 @@ export function createMissingMonitoringDataAlertType(): AlertTypeModel { alertParamsExpression: (props: any) => ( ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index f3e697bd270e..7fd9438e1cea 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -12,7 +12,7 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; -import { ALERT_REQUIRES_APP_CONTEXT } from '../../../common/constants'; +import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; interface ThreadPoolTypes { [key: string]: unknown; @@ -61,6 +61,6 @@ export function createThreadPoolRejectionsAlertType( return { errors }; }, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 8f6465633908..85211008ff7d 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -38,18 +38,18 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ELASTICSEARCH_SYSTEM_ID, - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_MISSING_MONITORING_DATA, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_MISSING_MONITORING_DATA, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -162,24 +162,24 @@ function renderLog(log) { ); } -const OVERVIEW_PANEL_ALERTS = [ - ALERT_CLUSTER_HEALTH, - ALERT_LICENSE_EXPIRATION, - ALERT_CCR_READ_EXCEPTIONS, +const OVERVIEW_PANEL_RULES = [ + RULE_CLUSTER_HEALTH, + RULE_LICENSE_EXPIRATION, + RULE_CCR_READ_EXCEPTIONS, ]; -const NODES_PANEL_ALERTS = [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_MISSING_MONITORING_DATA, +const NODES_PANEL_RULES = [ + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_MISSING_MONITORING_DATA, ]; -const INDICES_PANEL_ALERTS = [ALERT_LARGE_SHARD_SIZE]; +const INDICES_PANEL_RULES = [RULE_LARGE_SHARD_SIZE]; export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; @@ -286,8 +286,8 @@ export function ElasticsearchPanel(props) { }; let nodesAlertStatus = null; - if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS, setupModeContext)) { - const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, NODES_PANEL_RULES, setupModeContext)) { + const alertsList = NODES_PANEL_RULES.map((alertType) => alerts[alertType]); nodesAlertStatus = ( @@ -296,8 +296,8 @@ export function ElasticsearchPanel(props) { } let overviewAlertStatus = null; - if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_ALERTS, setupModeContext)) { - const alertsList = OVERVIEW_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_RULES, setupModeContext)) { + const alertsList = OVERVIEW_PANEL_RULES.map((alertType) => alerts[alertType]); overviewAlertStatus = ( @@ -306,8 +306,8 @@ export function ElasticsearchPanel(props) { } let indicesAlertStatus = null; - if (shouldShowAlertBadge(alerts, INDICES_PANEL_ALERTS, setupModeContext)) { - const alertsList = INDICES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, INDICES_PANEL_RULES, setupModeContext)) { + const alertsList = INDICES_PANEL_RULES.map((alertType) => alerts[alertType]); indicesAlertStatus = ( diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index ce09621b61df..654ef6590a06 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -29,7 +29,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { KIBANA_SYSTEM_ID, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -37,7 +37,7 @@ import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; -const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; +const INSTANCES_PANEL_ALERTS = [RULE_KIBANA_VERSION_MISMATCH]; export function KibanaPanel(props) { const setupMode = props.setupMode; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index b9c73f67e9f4..1afe75cda402 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -15,7 +15,7 @@ import { import { LOGSTASH, LOGSTASH_SYSTEM_ID, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../common/constants'; import { @@ -42,7 +42,7 @@ import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; -const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; +const NODES_PANEL_RULES = [RULE_LOGSTASH_VERSION_MISMATCH]; export function LogstashPanel(props) { const { setupMode } = props; @@ -72,8 +72,8 @@ export function LogstashPanel(props) { ) : null; let nodesAlertStatus = null; - if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS, setupModeContext)) { - const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, NODES_PANEL_RULES, setupModeContext)) { + const alertsList = NODES_PANEL_RULES.map((alertType) => alerts[alertType]); nodesAlertStatus = ( diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 15b7290fbeaf..710b453e7f21 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -23,9 +23,9 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_DETAILS, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_DETAILS, } from '../common/constants'; import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; @@ -176,14 +176,14 @@ export class MonitoringPlugin ruleTypeRegistry.register(createMissingMonitoringDataAlertType()); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS] + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS] ) ); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS] + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS] ) ); ruleTypeRegistry.register(createCCRReadExceptionsAlertType()); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js index fb9e1ae9a400..91cc9c8782b2 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js @@ -15,7 +15,7 @@ import { Ccr } from '../../../components/elasticsearch/ccr'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, + RULE_CCR_READ_EXCEPTIONS, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeRenderer } from '../../../components/renderers'; @@ -47,7 +47,7 @@ uiRoutes.when('/elasticsearch/ccr', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CCR_READ_EXCEPTIONS], + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index 21633bd03622..767fb1868563 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -16,7 +16,7 @@ import { MonitoringViewBaseController } from '../../../base_controller'; import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, + RULE_CCR_READ_EXCEPTIONS, ELASTICSEARCH_SYSTEM_ID, } from '../../../../../common/constants'; import { SetupModeRenderer } from '../../../../components/renderers'; @@ -46,7 +46,7 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CCR_READ_EXCEPTIONS], + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], filters: [ { shardId: $route.current.pathParams.shardId, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js index d975f523b752..927652795161 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -19,7 +19,7 @@ import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanc import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../../common/constants'; import { SetupModeContext } from '../../../../components/setup_mode/setup_mode_context'; @@ -80,7 +80,7 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], filters: [ { shardIndex: $route.current.pathParams.index, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js index e610e7c0b66b..c9efb622ff9d 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -21,7 +21,7 @@ import { Index } from '../../../components/elasticsearch/index/index'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -88,7 +88,7 @@ uiRoutes.when('/elasticsearch/indices/:index', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], filters: [ { shardIndex: $route.current.pathParams.index, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js index 0d8fe49e4e50..5acff8be20dc 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -16,7 +16,7 @@ import template from './index.html'; import { CODE_PATH_ELASTICSEARCH, ELASTICSEARCH_SYSTEM_ID, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, } from '../../../../common/constants'; import { SetupModeRenderer } from '../../../components/renderers'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -60,7 +60,7 @@ uiRoutes.when('/elasticsearch/indices', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 9fa96adabeea..dc0456178fbf 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -20,12 +20,12 @@ import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../../../common/constants'; function getPageData($injector) { @@ -77,12 +77,12 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], filters: [ { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index cb99a3c51996..3ec10aa9d4a4 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -22,12 +22,12 @@ import { nodesByIndices } from '../../../components/elasticsearch/shard_allocati import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -64,12 +64,12 @@ uiRoutes.when('/elasticsearch/nodes/:node', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], filters: [ { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index fd5ababcb493..5bc546e8590a 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -19,12 +19,12 @@ import { SetupModeRenderer } from '../../../components/renderers'; import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -94,12 +94,12 @@ uiRoutes.when('/elasticsearch/nodes', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], }, }, diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js index a318044330fe..a71289b08451 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -28,7 +28,7 @@ import { import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { CODE_PATH_KIBANA, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -77,7 +77,7 @@ uiRoutes.when('/kibana/instances/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 66fec51688ce..2601a366e684 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -18,7 +18,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA, - ALERT_KIBANA_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, } from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { @@ -48,7 +48,7 @@ uiRoutes.when('/kibana/instances', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js index 15d97a7dd52a..9acfd81d186f 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js @@ -29,7 +29,7 @@ import { import { MonitoringTimeseriesContainer } from '../../../../components/chart'; import { CODE_PATH_LOGSTASH, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../../common/constants'; import { AlertsCallout } from '../../../../alerts/callout'; @@ -77,7 +77,7 @@ uiRoutes.when('/logstash/node/:uuid/advanced', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, telemetryPageViewTitle: 'logstash_node_advanced', diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js index 80136c8aa9a2..b23875ba1a3b 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js @@ -27,7 +27,7 @@ import { } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { CODE_PATH_LOGSTASH, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -74,7 +74,7 @@ uiRoutes.when('/logstash/node/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, telemetryPageViewTitle: 'logstash_node', diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index b802af05be9a..56b5d0ec6c82 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -18,7 +18,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { @@ -48,7 +48,7 @@ uiRoutes.when('/logstash/nodes', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index a008170ac472..c86a5264b204 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -6,7 +6,7 @@ */ import { AlertsFactory } from './alerts_factory'; -import { ALERT_CPU_USAGE } from '../../common/constants'; +import { RULE_CPU_USAGE } from '../../common/constants'; jest.mock('../static_globals', () => ({ Globals: { @@ -39,7 +39,7 @@ describe('AlertsFactory', () => { ], }; }); - const alerts = await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); + const alerts = await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); expect(alerts).not.toBeNull(); expect(alerts.length).toBe(2); expect(alerts[0].getId()).toBe(1); @@ -54,7 +54,7 @@ describe('AlertsFactory', () => { total: 0, }; }); - await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); - expect(filter).toBe(`alert.attributes.alertTypeId:${ALERT_CPU_USAGE}`); + await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); + expect(filter).toBe(`alert.attributes.alertTypeId:${RULE_CPU_USAGE}`); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 24dd57d4f256..a276f96df009 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -6,64 +6,64 @@ */ import { - LargeShardSizeAlert, - CCRReadExceptionsAlert, - CpuUsageAlert, - MissingMonitoringDataAlert, - DiskUsageAlert, - ThreadPoolSearchRejectionsAlert, - ThreadPoolWriteRejectionsAlert, - MemoryUsageAlert, - NodesChangedAlert, - ClusterHealthAlert, - LicenseExpirationAlert, - LogstashVersionMismatchAlert, - KibanaVersionMismatchAlert, - ElasticsearchVersionMismatchAlert, - BaseAlert, + LargeShardSizeRule, + CCRReadExceptionsRule, + CpuUsageRule, + MissingMonitoringDataRule, + DiskUsageRule, + ThreadPoolSearchRejectionsRule, + ThreadPoolWriteRejectionsRule, + MemoryUsageRule, + NodesChangedRule, + ClusterHealthRule, + LicenseExpirationRule, + LogstashVersionMismatchRule, + KibanaVersionMismatchRule, + ElasticsearchVersionMismatchRule, + BaseRule, } from './'; import { - ALERT_CLUSTER_HEALTH, - ALERT_LICENSE_EXPIRATION, - ALERT_CPU_USAGE, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + RULE_CLUSTER_HEALTH, + RULE_LICENSE_EXPIRATION, + RULE_CPU_USAGE, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, } from '../../common/constants'; import { RulesClient } from '../../../alerting/server'; import { Alert } from '../../../alerting/common'; import { CommonAlertParams } from '../../common/types/alerts'; const BY_TYPE = { - [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, - [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, - [ALERT_CPU_USAGE]: CpuUsageAlert, - [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert, - [ALERT_DISK_USAGE]: DiskUsageAlert, - [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsAlert, - [ALERT_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsAlert, - [ALERT_MEMORY_USAGE]: MemoryUsageAlert, - [ALERT_NODES_CHANGED]: NodesChangedAlert, - [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, - [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert, - [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchAlert, - [ALERT_CCR_READ_EXCEPTIONS]: CCRReadExceptionsAlert, - [ALERT_LARGE_SHARD_SIZE]: LargeShardSizeAlert, + [RULE_CLUSTER_HEALTH]: ClusterHealthRule, + [RULE_LICENSE_EXPIRATION]: LicenseExpirationRule, + [RULE_CPU_USAGE]: CpuUsageRule, + [RULE_MISSING_MONITORING_DATA]: MissingMonitoringDataRule, + [RULE_DISK_USAGE]: DiskUsageRule, + [RULE_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsRule, + [RULE_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsRule, + [RULE_MEMORY_USAGE]: MemoryUsageRule, + [RULE_NODES_CHANGED]: NodesChangedRule, + [RULE_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchRule, + [RULE_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchRule, + [RULE_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchRule, + [RULE_CCR_READ_EXCEPTIONS]: CCRReadExceptionsRule, + [RULE_LARGE_SHARD_SIZE]: LargeShardSizeRule, }; export class AlertsFactory { public static async getByType( type: string, alertsClient: RulesClient | undefined - ): Promise { + ): Promise { const alertCls = BY_TYPE[type]; if (!alertCls || !alertsClient) { return []; @@ -77,7 +77,7 @@ export class AlertsFactory { if (!alertClientAlerts.total || !alertClientAlerts.data?.length) { return []; } - return alertClientAlerts.data.map((alert) => new alertCls(alert as Alert) as BaseAlert); + return alertClientAlerts.data.map((alert) => new alertCls(alert as Alert) as BaseRule); } public static getAll() { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts similarity index 80% rename from x-pack/plugins/monitoring/server/alerts/base_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/base_rule.test.ts index 3fe4eac71248..5234fcfce5cb 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; jest.mock('../static_globals', () => ({ Globals: { @@ -15,10 +15,10 @@ jest.mock('../static_globals', () => ({ }, })); -describe('BaseAlert', () => { +describe('BaseRule', () => { describe('create', () => { - it('should create an alert if it does not exist', async () => { - const alert = new BaseAlert(); + it('should create a rule if it does not exist', async () => { + const rule = new BaseRule(); const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { @@ -41,7 +41,7 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + await rule.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); expect(rulesClient.create).toHaveBeenCalledWith({ data: { actions: [ @@ -71,8 +71,8 @@ describe('BaseAlert', () => { }); }); - it('should not create an alert if it exists', async () => { - const alert = new BaseAlert(); + it('should not create a rule if it exists', async () => { + const rule = new BaseRule(); const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { @@ -96,7 +96,7 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + await rule.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); expect(rulesClient.create).not.toHaveBeenCalled(); }); }); @@ -116,8 +116,8 @@ describe('BaseAlert', () => { }; const id = '456def'; const filters: any[] = []; - const alert = new BaseAlert(); - const states = await alert.getStates(rulesClient as any, id, filters); + const rule = new BaseRule(); + const states = await rule.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({ abc123: { id: 'foobar', @@ -133,8 +133,8 @@ describe('BaseAlert', () => { }; const id = '456def'; const filters: any[] = []; - const alert = new BaseAlert(); - const states = await alert.getStates(rulesClient as any, id, filters); + const rule = new BaseRule(); + const states = await rule.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({}); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/base_alert.ts rename to x-pack/plugins/monitoring/server/alerts/base_rule.ts index 7bc5d4242d0b..62fb24560ddd 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -44,7 +44,7 @@ type ExecutedState = } | Record; -interface AlertOptions { +interface RuleOptions { id: string; name: string; throttle?: string | null; @@ -55,7 +55,7 @@ interface AlertOptions { accessorKey?: string; } -const defaultAlertOptions = (): AlertOptions => { +const defaultRuleOptions = (): RuleOptions => { return { id: '', name: '', @@ -65,24 +65,24 @@ const defaultAlertOptions = (): AlertOptions => { actionVariables: [], }; }; -export class BaseAlert { +export class BaseRule { protected scopedLogger: Logger; constructor( - public rawAlert?: SanitizedAlert, - public alertOptions: AlertOptions = defaultAlertOptions() + public sanitizedRule?: SanitizedAlert, + public ruleOptions: RuleOptions = defaultRuleOptions() ) { - const defaultOptions = defaultAlertOptions(); + const defaultOptions = defaultRuleOptions(); defaultOptions.defaultParams = { ...defaultOptions.defaultParams, - ...this.alertOptions.defaultParams, + ...this.ruleOptions.defaultParams, }; - this.alertOptions = { ...defaultOptions, ...this.alertOptions }; - this.scopedLogger = Globals.app.getLogger(alertOptions.id); + this.ruleOptions = { ...defaultOptions, ...this.ruleOptions }; + this.scopedLogger = Globals.app.getLogger(ruleOptions.id); } - public getAlertType(): AlertType { - const { id, name, actionVariables } = this.alertOptions; + public getRuleType(): AlertType { + const { id, name, actionVariables } = this.ruleOptions; return { id, name, @@ -110,7 +110,7 @@ export class BaseAlert { } public getId() { - return this.rawAlert?.id; + return this.sanitizedRule?.id; } public async createIfDoesNotExist( @@ -118,24 +118,24 @@ export class BaseAlert { actionsClient: ActionsClient, actions: AlertEnableAction[] ): Promise> { - const existingAlertData = await rulesClient.find({ + const existingRuleData = await rulesClient.find({ options: { - search: this.alertOptions.id, + search: this.ruleOptions.id, }, }); - if (existingAlertData.total > 0) { - const existingAlert = existingAlertData.data[0] as Alert; - return existingAlert; + if (existingRuleData.total > 0) { + const existingRule = existingRuleData.data[0] as Alert; + return existingRule; } - const alertActions = []; + const ruleActions = []; for (const actionData of actions) { const action = await actionsClient.get({ id: actionData.id }); if (!action) { continue; } - alertActions.push({ + ruleActions.push({ group: 'default', id: actionData.id, params: { @@ -151,7 +151,7 @@ export class BaseAlert { id: alertTypeId, throttle = '1d', interval = '1m', - } = this.alertOptions; + } = this.ruleOptions; return await rulesClient.create({ data: { enabled: true, @@ -163,7 +163,7 @@ export class BaseAlert { throttle, notifyWhen: null, schedule: { interval }, - actions: alertActions, + actions: ruleActions, }, }); } @@ -247,11 +247,11 @@ export class BaseAlert { return await fetchClusters(esClient, esIndexPattern); } const limit = parseDuration(params.limit); - const rangeFilter = this.alertOptions.fetchClustersRange + const rangeFilter = this.ruleOptions.fetchClustersRange ? { timestamp: { format: 'epoch_millis', - gte: +new Date() - limit - this.alertOptions.fetchClustersRange, + gte: +new Date() - limit - this.ruleOptions.fetchClustersRange, }, } : undefined; @@ -281,7 +281,7 @@ export class BaseAlert { continue; } - const key = this.alertOptions.accessorKey; + const key = this.ruleOptions.accessorKey; // for each node, update the alert's state with node state for (const node of nodes) { diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index 5c8ef7abbbf5..8dd4623bfd7e 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { CCRReadExceptionsAlert } from './ccr_read_exceptions_alert'; -import { ALERT_CCR_READ_EXCEPTIONS } from '../../common/constants'; +import { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; +import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type ICCRReadExceptionsAlertMock = CCRReadExceptionsAlert & { +type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & { defaultParams: { duration: string; }; @@ -47,16 +47,16 @@ jest.mock('../static_globals', () => ({ }, })); -describe('CCRReadExceptionsAlert', () => { +describe('CCRReadExceptionsRule', () => { it('should have defaults', () => { - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_CCR_READ_EXCEPTIONS); - expect(alert.alertOptions.name).toBe('CCR read exceptions'); - expect(alert.alertOptions.throttle).toBe('6h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS); + expect(rule.ruleOptions.name).toBe('CCR read exceptions'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ duration: '1h', }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'remoteCluster', description: 'The remote cluster experiencing CCR read exceptions.', @@ -146,11 +146,11 @@ describe('CCRReadExceptionsAlert', () => { }); it('should fire actions', async () => { - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - const type = alert.getAlertType(); + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, @@ -177,11 +177,11 @@ describe('CCRReadExceptionsAlert', () => { }, ]; }); - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - const type = alert.getAlertType(); + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts rename to x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 28f562b2cb13..587b3a69fb76 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -24,8 +24,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_DETAILS, + RULE_CCR_READ_EXCEPTIONS, + RULE_DETAILS, } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; @@ -36,11 +36,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class CCRReadExceptionsAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CCR_READ_EXCEPTIONS, - name: ALERT_DETAILS[ALERT_CCR_READ_EXCEPTIONS].label, +export class CCRReadExceptionsRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CCR_READ_EXCEPTIONS, + name: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].label, throttle: '6h', defaultParams: { duration: '1h', diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts similarity index 89% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 68d0f7b1e0ce..5d209f7fc4a8 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ClusterHealthAlert } from './cluster_health_alert'; -import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { ClusterHealthRule } from './cluster_health_rule'; +import { RULE_CLUSTER_HEALTH } from '../../common/constants'; import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -35,13 +35,13 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); -describe('ClusterHealthAlert', () => { +describe('ClusterHealthRule', () => { it('should have defaults', () => { - const alert = new ClusterHealthAlert(); - expect(alert.alertOptions.id).toBe(ALERT_CLUSTER_HEALTH); - expect(alert.alertOptions.name).toBe('Cluster health'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ClusterHealthRule(); + expect(rule.ruleOptions.id).toBe(RULE_CLUSTER_HEALTH); + expect(rule.ruleOptions.name).toBe('Cluster health'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'clusterHealth', description: 'The health of the cluster.' }, { name: 'internalShortMessage', @@ -112,8 +112,8 @@ describe('ClusterHealthAlert', () => { }); it('should fire actions', async () => { - const alert = new ClusterHealthAlert(); - const type = alert.getAlertType(); + const rule = new ClusterHealthRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: {}, @@ -179,8 +179,8 @@ describe('ClusterHealthAlert', () => { }, ]; }); - const alert = new ClusterHealthAlert(); - const type = alert.getAlertType(); + const rule = new ClusterHealthRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: {}, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts rename to x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index c5983ae9897f..7fac3b74a1b6 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -20,8 +20,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_CLUSTER_HEALTH, - LEGACY_ALERT_DETAILS, + RULE_CLUSTER_HEALTH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; @@ -43,11 +43,11 @@ const YELLOW_STATUS_MESSAGE = i18n.translate( } ); -export class ClusterHealthAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CLUSTER_HEALTH, - name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, +export class ClusterHealthRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CLUSTER_HEALTH, + name: LEGACY_RULE_DETAILS[RULE_CLUSTER_HEALTH].label, actionVariables: [ { name: 'clusterHealth', diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts similarity index 89% rename from x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index 81c51366cb8a..9b19c1ddeb7d 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CpuUsageAlert } from './cpu_usage_alert'; -import { ALERT_CPU_USAGE } from '../../common/constants'; +import { CpuUsageRule } from './cpu_usage_rule'; +import { RULE_CPU_USAGE } from '../../common/constants'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -35,14 +35,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('CpuUsageAlert', () => { +describe('CpuUsageRule', () => { it('should have defaults', () => { - const alert = new CpuUsageAlert(); - expect(alert.alertOptions.id).toBe(ALERT_CPU_USAGE); - expect(alert.alertOptions.name).toBe('CPU Usage'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new CpuUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE); + expect(rule.ruleOptions.name).toBe('CPU Usage'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high cpu usage.' }, { name: 'internalShortMessage', @@ -114,11 +114,11 @@ describe('CpuUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -211,11 +211,11 @@ describe('CpuUsageAlert', () => { }, ]; }); - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -233,11 +233,11 @@ describe('CpuUsageAlert', () => { }, ]; }); - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index 44b3cb306bd9..2e57a3c22de1 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -23,11 +23,7 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH, RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; @@ -39,11 +35,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class CpuUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CPU_USAGE, - name: ALERT_DETAILS[ALERT_CPU_USAGE].label, +export class CpuUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CPU_USAGE, + name: RULE_DETAILS[RULE_CPU_USAGE].label, accessorKey: 'cpuUsage', defaultParams: { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts similarity index 85% rename from x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index 0514e7b3cb07..63ff6a7ccab9 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { DiskUsageAlert } from './disk_usage_alert'; -import { ALERT_DISK_USAGE } from '../../common/constants'; +import { DiskUsageRule } from './disk_usage_rule'; +import { RULE_DISK_USAGE } from '../../common/constants'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type IDiskUsageAlertMock = DiskUsageAlert & { +type IDiskUsageAlertMock = DiskUsageRule & { defaultParams: { threshold: number; duration: string; @@ -48,14 +48,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('DiskUsageAlert', () => { +describe('DiskUsageRule', () => { it('should have defaults', () => { - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_DISK_USAGE); - expect(alert.alertOptions.name).toBe('Disk Usage'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const alert = new DiskUsageRule() as IDiskUsageAlertMock; + expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE); + expect(alert.ruleOptions.name).toBe('Disk Usage'); + expect(alert.ruleOptions.throttle).toBe('1d'); + expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); + expect(alert.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high disk usage.' }, { name: 'internalShortMessage', @@ -126,11 +126,11 @@ describe('DiskUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - const type = alert.getAlertType(); + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { @@ -156,11 +156,11 @@ describe('DiskUsageAlert', () => { }, ]; }); - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - const type = alert.getAlertType(); + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index ac7829d121a3..ae3025c1db92 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -23,11 +23,7 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - ALERT_DISK_USAGE, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH, RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; @@ -38,11 +34,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class DiskUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_DISK_USAGE, - name: ALERT_DETAILS[ALERT_DISK_USAGE].label, +export class DiskUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_DISK_USAGE, + name: RULE_DETAILS[RULE_DISK_USAGE].label, accessorKey: 'diskUsage', defaultParams: { threshold: 80, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 3ac15d795d02..12fa54f34e3c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule'; +import { RULE_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -38,11 +38,11 @@ jest.mock('../static_globals', () => ({ describe('ElasticsearchVersionMismatchAlert', () => { it('should have defaults', () => { - const alert = new ElasticsearchVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_ELASTICSEARCH_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Elasticsearch version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ElasticsearchVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_ELASTICSEARCH_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Elasticsearch version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Elasticsearch running in this cluster.', @@ -116,12 +116,12 @@ describe('ElasticsearchVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new ElasticsearchVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new ElasticsearchVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -170,12 +170,12 @@ describe('ElasticsearchVersionMismatchAlert', () => { }, ]; }); - const alert = new ElasticsearchVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new ElasticsearchVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts similarity index 93% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index d51eb99e3a47..6a5abcb4975f 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; -export class ElasticsearchVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, +export class ElasticsearchVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_ELASTICSEARCH_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_ELASTICSEARCH_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index e6398273b46b..d5c59c802b68 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -5,19 +5,19 @@ * 2.0. */ -export { LargeShardSizeAlert } from './large_shard_size_alert'; -export { CCRReadExceptionsAlert } from './ccr_read_exceptions_alert'; -export { BaseAlert } from './base_alert'; -export { CpuUsageAlert } from './cpu_usage_alert'; -export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; -export { DiskUsageAlert } from './disk_usage_alert'; -export { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; -export { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; -export { MemoryUsageAlert } from './memory_usage_alert'; -export { ClusterHealthAlert } from './cluster_health_alert'; -export { LicenseExpirationAlert } from './license_expiration_alert'; -export { NodesChangedAlert } from './nodes_changed_alert'; -export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; -export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; -export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +export { LargeShardSizeRule } from './large_shard_size_rule'; +export { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; +export { BaseRule } from './base_rule'; +export { CpuUsageRule } from './cpu_usage_rule'; +export { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; +export { DiskUsageRule } from './disk_usage_rule'; +export { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; +export { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; +export { MemoryUsageRule } from './memory_usage_rule'; +export { ClusterHealthRule } from './cluster_health_rule'; +export { LicenseExpirationRule } from './license_expiration_rule'; +export { NodesChangedRule } from './nodes_changed_rule'; +export { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule'; +export { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; +export { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; export { AlertsFactory } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index 02a8f59aecfb..01016a7c02ae 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; -import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; +import { RULE_KIBANA_VERSION_MISMATCH } from '../../common/constants'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -36,13 +36,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('KibanaVersionMismatchAlert', () => { +describe('KibanaVersionMismatchRule', () => { it('should have defaults', () => { - const alert = new KibanaVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_KIBANA_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Kibana version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new KibanaVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_KIBANA_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Kibana version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Kibana running in this cluster.', @@ -119,11 +119,11 @@ describe('KibanaVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new KibanaVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new KibanaVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -172,11 +172,11 @@ describe('KibanaVersionMismatchAlert', () => { }, ]; }); - const alert = new KibanaVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new KibanaVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 3d6417e8fd64..90275ea4d23a 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_KIBANA_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_KIBANA_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_KIBANA, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; -export class KibanaVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_KIBANA_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, +export class KibanaVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_KIBANA_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_KIBANA_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts similarity index 85% rename from x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index 18987a24e552..0b8509c4fa56 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { LargeShardSizeAlert } from './large_shard_size_alert'; -import { ALERT_LARGE_SHARD_SIZE } from '../../common/constants'; +import { LargeShardSizeRule } from './large_shard_size_rule'; +import { RULE_LARGE_SHARD_SIZE } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type ILargeShardSizeAlertMock = LargeShardSizeAlert & { +type ILargeShardSizeRuleMock = LargeShardSizeRule & { defaultParams: { threshold: number; duration: string; @@ -48,17 +48,17 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LargeShardSizeAlert', () => { +describe('LargeShardSizeRule', () => { it('should have defaults', () => { - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_LARGE_SHARD_SIZE); - expect(alert.alertOptions.name).toBe('Shard size'); - expect(alert.alertOptions.throttle).toBe('12h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE); + expect(rule.ruleOptions.name).toBe('Shard size'); + expect(rule.ruleOptions.throttle).toBe('12h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 55, indexPattern: '-.*', }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'shardIndex', description: 'The index experiencing large average shard size.' }, { name: 'internalShortMessage', @@ -130,11 +130,11 @@ describe('LargeShardSizeAlert', () => { }); it('should fire actions', async () => { - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - const type = alert.getAlertType(); + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, @@ -158,11 +158,11 @@ describe('LargeShardSizeAlert', () => { }, ]; }); - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - const type = alert.getAlertType(); + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts rename to x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index a365e530cbd0..86f96daa3b21 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -24,8 +24,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, - ALERT_DETAILS, + RULE_LARGE_SHARD_SIZE, + RULE_DETAILS, } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; @@ -35,11 +35,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class LargeShardSizeAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LARGE_SHARD_SIZE, - name: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].label, +export class LargeShardSizeRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LARGE_SHARD_SIZE, + name: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].label, throttle: '12h', defaultParams: { indexPattern: '-.*', threshold: 55 }, actionVariables: [ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts similarity index 87% rename from x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 0bb8ba23cd49..b8d00cac5c88 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { LicenseExpirationAlert } from './license_expiration_alert'; -import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; +import { LicenseExpirationRule } from './license_expiration_rule'; +import { RULE_LICENSE_EXPIRATION } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -42,13 +42,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LicenseExpirationAlert', () => { +describe('LicenseExpirationRule', () => { it('should have defaults', () => { - const alert = new LicenseExpirationAlert(); - expect(alert.alertOptions.id).toBe(ALERT_LICENSE_EXPIRATION); - expect(alert.alertOptions.name).toBe('License expiration'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new LicenseExpirationRule(); + expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION); + expect(rule.ruleOptions.name).toBe('License expiration'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'expiredDate', description: 'The date when the license expires.' }, { name: 'clusterName', description: 'The cluster to which the license belong.' }, { @@ -117,11 +117,11 @@ describe('LicenseExpirationAlert', () => { }); it('should fire actions', async () => { - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const alert = new LicenseExpirationRule(); + const type = alert.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: alert.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -196,12 +196,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); @@ -218,12 +218,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); }); @@ -239,12 +239,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts rename to x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index f5a6f2f7c7e1..67ea8bd57b49 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -22,8 +22,8 @@ import { } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; import { - ALERT_LICENSE_EXPIRATION, - LEGACY_ALERT_DETAILS, + RULE_LICENSE_EXPIRATION, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -36,11 +36,11 @@ import { fetchLicenses } from '../lib/alerts/fetch_licenses'; const EXPIRES_DAYS = [60, 30, 14, 7]; -export class LicenseExpirationAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LICENSE_EXPIRATION, - name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, +export class LicenseExpirationRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LICENSE_EXPIRATION, + name: LEGACY_RULE_DETAILS[RULE_LICENSE_EXPIRATION].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 9f0096a7e298..20f64b65ba1f 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; -import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; +import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,13 +37,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LogstashVersionMismatchAlert', () => { +describe('LogstashVersionMismatchRule', () => { it('should have defaults', () => { - const alert = new LogstashVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_LOGSTASH_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Logstash version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new LogstashVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_LOGSTASH_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Logstash version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Logstash running in this cluster.', @@ -117,12 +117,12 @@ describe('LogstashVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new LogstashVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new LogstashVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -171,12 +171,12 @@ describe('LogstashVersionMismatchAlert', () => { }, ]; }); - const alert = new LogstashVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new LogstashVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts similarity index 93% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 7ee478b17fff..0f9ad4dd4b11 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_LOGSTASH_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_LOGSTASH_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_LOGSTASH, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; -export class LogstashVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LOGSTASH_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, +export class LogstashVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LOGSTASH_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_LOGSTASH_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 4076eff956ee..8547f126a02d 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { MemoryUsageAlert } from './memory_usage_alert'; -import { ALERT_MEMORY_USAGE } from '../../common/constants'; +import { MemoryUsageRule } from './memory_usage_rule'; +import { RULE_MEMORY_USAGE } from '../../common/constants'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -35,14 +35,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('MemoryUsageAlert', () => { +describe('MemoryUsageRule', () => { it('should have defaults', () => { - const alert = new MemoryUsageAlert(); - expect(alert.alertOptions.id).toBe(ALERT_MEMORY_USAGE); - expect(alert.alertOptions.name).toBe('Memory Usage (JVM)'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new MemoryUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE); + expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high memory usage.' }, { name: 'internalShortMessage', @@ -114,11 +114,11 @@ describe('MemoryUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -245,11 +245,11 @@ describe('MemoryUsageAlert', () => { }, ]; }); - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -267,11 +267,11 @@ describe('MemoryUsageAlert', () => { }, ]; }); - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 86357e7b6f0e..384610e659d4 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -25,8 +25,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_MEMORY_USAGE, - ALERT_DETAILS, + RULE_MEMORY_USAGE, + RULE_DETAILS, } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -39,11 +39,11 @@ import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { Globals } from '../static_globals'; -export class MemoryUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_MEMORY_USAGE, - name: ALERT_DETAILS[ALERT_MEMORY_USAGE].label, +export class MemoryUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_MEMORY_USAGE, + name: RULE_DETAILS[RULE_MEMORY_USAGE].label, accessorKey: 'memoryUsage', defaultParams: { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts similarity index 88% rename from x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index c6bf853b7787..88ddc7c04884 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; -import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants'; +import { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; +import { RULE_MISSING_MONITORING_DATA } from '../../common/constants'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,14 +37,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('MissingMonitoringDataAlert', () => { +describe('MissingMonitoringDataRule', () => { it('should have defaults', () => { - const alert = new MissingMonitoringDataAlert(); - expect(alert.alertOptions.id).toBe(ALERT_MISSING_MONITORING_DATA); - expect(alert.alertOptions.name).toBe('Missing monitoring data'); - expect(alert.alertOptions.throttle).toBe('6h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new MissingMonitoringDataRule(); + expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA); + expect(rule.ruleOptions.name).toBe('Missing monitoring data'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node missing monitoring data.' }, { name: 'internalShortMessage', @@ -118,11 +118,11 @@ describe('MissingMonitoringDataAlert', () => { }); it('should fire actions', async () => { - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -202,12 +202,12 @@ describe('MissingMonitoringDataAlert', () => { }, ]; }); - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -225,12 +225,12 @@ describe('MissingMonitoringDataAlert', () => { }, ]; }); - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 77581df2303b..32e4ff738c71 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -20,11 +20,7 @@ import { AlertNodeState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN, - ALERT_MISSING_MONITORING_DATA, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN, RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -37,11 +33,11 @@ import { Globals } from '../static_globals'; // Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back const LIMIT_BUFFER = 3 * 60 * 1000; -export class MissingMonitoringDataAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_MISSING_MONITORING_DATA, - name: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label, +export class MissingMonitoringDataRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_MISSING_MONITORING_DATA, + name: RULE_DETAILS[RULE_MISSING_MONITORING_DATA].label, accessorKey: 'gapDuration', fetchClustersRange: LIMIT_BUFFER, defaultParams: { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts similarity index 92% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index d628c1c30a7e..199e50c80ef4 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { NodesChangedAlert } from './nodes_changed_alert'; -import { ALERT_NODES_CHANGED } from '../../common/constants'; +import { NodesChangedRule } from './nodes_changed_rule'; +import { RULE_NODES_CHANGED } from '../../common/constants'; import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -44,11 +44,11 @@ jest.mock('../static_globals', () => ({ describe('NodesChangedAlert', () => { it('should have defaults', () => { - const alert = new NodesChangedAlert(); - expect(alert.alertOptions.id).toBe(ALERT_NODES_CHANGED); - expect(alert.alertOptions.name).toBe('Nodes changed'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new NodesChangedRule(); + expect(rule.ruleOptions.id).toBe(RULE_NODES_CHANGED); + expect(rule.ruleOptions.name).toBe('Nodes changed'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'added', description: 'The list of nodes added to the cluster.' }, { name: 'removed', description: 'The list of nodes removed from the cluster.' }, { name: 'restarted', description: 'The list of nodes restarted in the cluster.' }, @@ -168,12 +168,12 @@ describe('NodesChangedAlert', () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesChanged; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -231,12 +231,12 @@ describe('NodesChangedAlert', () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesAddedChangedRemoved; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -325,12 +325,12 @@ describe('NodesChangedAlert', () => { }, ]; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts rename to x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index b26008ff3860..90bd70f32c8c 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -21,8 +21,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_NODES_CHANGED, - LEGACY_ALERT_DETAILS, + RULE_NODES_CHANGED, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; @@ -63,11 +63,11 @@ function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { }; } -export class NodesChangedAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_NODES_CHANGED, - name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, +export class NodesChangedRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_NODES_CHANGED, + name: LEGACY_RULE_DETAILS[RULE_NODES_CHANGED].label, actionVariables: [ { name: 'added', diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts similarity index 97% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index 3b11d3464a21..c478b2f687c0 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -32,7 +32,7 @@ import { Globals } from '../static_globals'; type ActionVariables = Array<{ name: string; description: string }>; -export class ThreadPoolRejectionsAlertBase extends BaseAlert { +export class ThreadPoolRejectionsRuleBase extends BaseRule { protected static createActionVariables(type: string) { return [ { @@ -50,13 +50,13 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { } constructor( - rawAlert: Alert | undefined = undefined, + sanitizedRule: Alert | undefined = undefined, public readonly id: string, public readonly threadPoolType: string, public readonly name: string, public readonly actionVariables: ActionVariables ) { - super(rawAlert, { + super(sanitizedRule, { id, name, defaultParams: { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts deleted file mode 100644 index 38b99fb2c6ea..000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts +++ /dev/null @@ -1,27 +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 { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; -import { ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; -import { Alert } from '../../../alerting/common'; - -export class ThreadPoolSearchRejectionsAlert extends ThreadPoolRejectionsAlertBase { - private static TYPE = ALERT_THREAD_POOL_SEARCH_REJECTIONS; - private static THREAD_POOL_TYPE = 'search'; - private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS].label; - constructor(rawAlert?: Alert) { - super( - rawAlert, - ThreadPoolSearchRejectionsAlert.TYPE, - ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE, - ThreadPoolSearchRejectionsAlert.LABEL, - ThreadPoolRejectionsAlertBase.createActionVariables( - ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE - ) - ); - } -} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 78f3e937016a..351980d3f385 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; -import { ALERT_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants'; +import { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; +import { RULE_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,14 +37,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('ThreadpoolSearchRejectionsAlert', () => { +describe('ThreadpoolSearchRejectionsRule', () => { it('should have defaults', () => { - const alert = new ThreadPoolSearchRejectionsAlert(); - expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_SEARCH_REJECTIONS); - expect(alert.alertOptions.name).toBe('Thread pool search rejections'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ThreadPoolSearchRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS); + expect(rule.ruleOptions.name).toBe('Thread pool search rejections'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high thread pool search rejections.' }, { name: 'internalShortMessage', @@ -120,11 +120,11 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }); it('should fire actions', async () => { - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -251,12 +251,12 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -274,12 +274,12 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts new file mode 100644 index 000000000000..509c50cda70b --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ThreadPoolRejectionsRuleBase } from './thread_pool_rejections_rule_base'; +import { RULE_THREAD_POOL_SEARCH_REJECTIONS, RULE_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerting/common'; + +export class ThreadPoolSearchRejectionsRule extends ThreadPoolRejectionsRuleBase { + private static TYPE = RULE_THREAD_POOL_SEARCH_REJECTIONS; + private static THREAD_POOL_TYPE = 'search'; + private static readonly LABEL = RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS].label; + constructor(sanitizedRule?: Alert) { + super( + sanitizedRule, + ThreadPoolSearchRejectionsRule.TYPE, + ThreadPoolSearchRejectionsRule.THREAD_POOL_TYPE, + ThreadPoolSearchRejectionsRule.LABEL, + ThreadPoolRejectionsRuleBase.createActionVariables( + ThreadPoolSearchRejectionsRule.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts deleted file mode 100644 index 6df8411e1ec8..000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts +++ /dev/null @@ -1,27 +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 { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; -import { ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; -import { Alert } from '../../../alerting/common'; - -export class ThreadPoolWriteRejectionsAlert extends ThreadPoolRejectionsAlertBase { - private static TYPE = ALERT_THREAD_POOL_WRITE_REJECTIONS; - private static THREAD_POOL_TYPE = 'write'; - private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS].label; - constructor(rawAlert?: Alert) { - super( - rawAlert, - ThreadPoolWriteRejectionsAlert.TYPE, - ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE, - ThreadPoolWriteRejectionsAlert.LABEL, - ThreadPoolRejectionsAlertBase.createActionVariables( - ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE - ) - ); - } -} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index 83ba6fc7532a..79896d11da2c 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; -import { ALERT_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants'; +import { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; +import { RULE_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -39,12 +39,12 @@ jest.mock('../static_globals', () => ({ describe('ThreadpoolWriteRejectionsAlert', () => { it('should have defaults', () => { - const alert = new ThreadPoolWriteRejectionsAlert(); - expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_WRITE_REJECTIONS); - expect(alert.alertOptions.name).toBe(`Thread pool write rejections`); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ThreadPoolWriteRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS); + expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high thread pool write rejections.' }, { name: 'internalShortMessage', @@ -120,11 +120,11 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }); it('should fire actions', async () => { - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -251,12 +251,12 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -274,12 +274,12 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts new file mode 100644 index 000000000000..ca7e1dc67d5b --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ThreadPoolRejectionsRuleBase } from './thread_pool_rejections_rule_base'; +import { RULE_THREAD_POOL_WRITE_REJECTIONS, RULE_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerting/common'; + +export class ThreadPoolWriteRejectionsRule extends ThreadPoolRejectionsRuleBase { + private static TYPE = RULE_THREAD_POOL_WRITE_REJECTIONS; + private static THREAD_POOL_TYPE = 'write'; + private static readonly LABEL = RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS].label; + constructor(sanitizedRule?: Alert) { + super( + sanitizedRule, + ThreadPoolWriteRejectionsRule.TYPE, + ThreadPoolWriteRejectionsRule.THREAD_POOL_TYPE, + ThreadPoolWriteRejectionsRule.LABEL, + ThreadPoolRejectionsRuleBase.createActionVariables( + ThreadPoolWriteRejectionsRule.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 45b3e0720068..9a5699189241 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -7,8 +7,7 @@ import fs from 'fs'; import { when } from 'jest-when'; - -import { createConfig, configSchema } from './config'; +import { configSchema, createConfig } from './config'; const MOCKED_PATHS = [ '/proc/self/cgroup', @@ -71,6 +70,8 @@ describe('config schema', () => { "enabled": false, }, }, + "debug_log_path": "", + "debug_mode": false, "elasticsearch": Object { "apiVersion": "master", "customHeaders": Object {}, diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 8c411fb5c28a..98fd02b03539 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -25,6 +25,8 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), + debug_mode: schema.boolean({ defaultValue: false }), + debug_log_path: schema.string({ defaultValue: '' }), ccs: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), diff --git a/x-pack/plugins/monitoring/server/debug_logger.ts b/x-pack/plugins/monitoring/server/debug_logger.ts new file mode 100644 index 000000000000..0add1f12f030 --- /dev/null +++ b/x-pack/plugins/monitoring/server/debug_logger.ts @@ -0,0 +1,85 @@ +/* + * 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 fs from 'fs'; +import { MonitoringConfig } from './config'; +import { RouteDependencies } from './types'; + +export function decorateDebugServer( + _server: any, + config: MonitoringConfig, + logger: RouteDependencies['logger'] +) { + // bail if the proper config value is not set (extra protection) + if (!config.ui.debug_mode) { + return _server; + } + + // create a debug logger that will either write to file (if debug_log_path exists) or log out via logger + const debugLog = createDebugLogger({ path: config.ui.debug_log_path, logger }); + + return { + // maintain the rest of _server untouched + ..._server, + // TODO: replace any + route: (options: any) => { + const apiPath = options.path; + return _server.route({ + ...options, + // TODO: replace any + handler: async (req: any) => { + const { elasticsearch: cached } = req.server.plugins; + const apiRequestHeaders = req.headers; + req.server.plugins.elasticsearch = { + ...req.server.plugins.elasticsearch, + getCluster: (name: string) => { + const cluster = cached.getCluster(name); + return { + ...cluster, + // TODO: better types? + callWithRequest: async (_req: typeof req, type: string, params: any) => { + const result = await cluster.callWithRequest(_req, type, params); + + // log everything about this request -> query -> result + debugLog({ + api_path: apiPath, + referer_url: apiRequestHeaders.referer, + query: { + params, + result, + }, + }); + + return result; + }, + }; + }, + }; + return options.handler(req); + }, + }); + }, + }; +} + +function createDebugLogger({ + path, + logger, +}: { + path: string; + logger: RouteDependencies['logger']; +}) { + if (path.length > 0) { + const stream = fs.createWriteStream('./stack_monitoring_debug_log.ndjson', { flags: 'a' }); + return function logToFile(line: any) { + stream.write(JSON.stringify(line)); + }; + } else { + return function logToStdOut(line: any) { + logger.info(JSON.stringify(line)); + }; + } +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index 3662b8252f0a..6e6f9469164e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -9,10 +9,10 @@ import { fetchStatus } from './fetch_status'; import { AlertUiState, AlertState } from '../../../common/types/alerts'; import { AlertSeverity } from '../../../common/enums'; import { - ALERT_CPU_USAGE, - ALERT_CLUSTER_HEALTH, - ALERT_DISK_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_CLUSTER_HEALTH, + RULE_DISK_USAGE, + RULE_MISSING_MONITORING_DATA, } from '../../../common/constants'; jest.mock('../../static_globals', () => ({ @@ -31,7 +31,7 @@ jest.mock('../../static_globals', () => ({ })); describe('fetchStatus', () => { - const alertType = ALERT_CPU_USAGE; + const alertType = RULE_CPU_USAGE; const alertTypes = [alertType]; const defaultClusterState = { clusterUuid: 'abc', @@ -81,11 +81,11 @@ describe('fetchStatus', () => { expect(status).toEqual({ monitoring_alert_cpu_usage: [ { - rawAlert: { id: 1 }, + sanitizedRule: { id: 1 }, states: [], }, { - rawAlert: { id: 2 }, + sanitizedRule: { id: 2 }, states: [], }, ], @@ -149,11 +149,7 @@ describe('fetchStatus', () => { isEnabled: true, })), }; - await fetchStatus( - rulesClient as any, - [ALERT_CLUSTER_HEALTH], - [defaultClusterState.clusterUuid] - ); + await fetchStatus(rulesClient as any, [RULE_CLUSTER_HEALTH], [defaultClusterState.clusterUuid]); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); @@ -187,13 +183,13 @@ describe('fetchStatus', () => { }; const status = await fetchStatus( customRulesClient as any, - [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + [RULE_CPU_USAGE, RULE_DISK_USAGE, RULE_MISSING_MONITORING_DATA], [defaultClusterState.clusterUuid] ); expect(Object.keys(status)).toEqual([ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_MISSING_MONITORING_DATA, ]); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6355cb3eb26b..257f0cec4860 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -9,7 +9,7 @@ import { AlertInstanceState } from '../../../common/types/alerts'; import { RulesClient } from '../../../../alerting/server'; import { AlertsFactory } from '../../alerts'; import { CommonAlertState, CommonAlertFilter, RulesByType } from '../../../common/types/alerts'; -import { ALERTS } from '../../../common/constants'; +import { RULES } from '../../../common/constants'; export async function fetchStatus( rulesClient: RulesClient, @@ -18,7 +18,7 @@ export async function fetchStatus( filters: CommonAlertFilter[] = [] ): Promise { const rulesByType = await Promise.all( - (alertTypes || ALERTS).map(async (type) => AlertsFactory.getByType(type, rulesClient)) + (alertTypes || RULES).map(async (type) => AlertsFactory.getByType(type, rulesClient)) ); if (!rulesByType.length) return {}; @@ -26,13 +26,13 @@ export async function fetchStatus( const rulesWithStates = await Promise.all( rulesFlattened.map(async (rule) => { // we should have a different class to distinguish between "alerts" where the rule exists - // and a BaseAlert created without an existing rule for better typing so we don't need to check here - if (!rule.rawAlert) { - throw new Error('alert missing rawAlert'); + // and a BaseRule created without an existing rule for better typing so we don't need to check here + if (!rule.sanitizedRule) { + throw new Error('alert missing sanitizedRule'); } const id = rule.getId(); if (!id) { - throw new Error('alert missing id'); + throw new Error('rule missing id'); } // Now that we have the id, we can get the state @@ -63,10 +63,10 @@ export async function fetchStatus( [] ); - const type = rule.alertOptions.id; + const type = rule.ruleOptions.id; const result = { states: alertStates, - rawAlert: rule.rawAlert, + sanitizedRule: rule.sanitizedRule, }; return { type, result }; }) diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index aed48b739152..1d6af9f080dc 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,53 +6,52 @@ */ import Boom from '@hapi/boom'; -import { i18n } from '@kbn/i18n'; -import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { - Logger, - PluginInitializerContext, - KibanaRequest, - KibanaResponseFactory, CoreSetup, - ICustomClusterClient, CoreStart, CustomHttpResponseOptions, - ResponseError, + ICustomClusterClient, + KibanaRequest, + KibanaResponseFactory, + Logger, Plugin, + PluginInitializerContext, + ResponseError, SharedGlobalConfig, } from 'kibana/server'; +import { get, has } from 'lodash'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { - LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, KIBANA_STATS_TYPE_MONITORING, - ALERTS, + RULES, + LOGGING_TAG, SAVED_OBJECT_TELEMETRY, } from '../common/constants'; -import { MonitoringConfig, createConfig, configSchema } from './config'; -import { requireUIRoutes } from './routes'; +import { AlertsFactory } from './alerts'; +import { configSchema, createConfig, MonitoringConfig } from './config'; +import { instantiateClient } from './es_client/instantiate_client'; import { initBulkUploader } from './kibana_monitoring'; -import { initInfraSource } from './lib/logs/init_infra_source'; import { registerCollectors } from './kibana_monitoring/collectors'; -import { registerMonitoringTelemetryCollection } from './telemetry_collection'; +import { initInfraSource } from './lib/logs/init_infra_source'; import { LicenseService } from './license_service'; -import { AlertsFactory } from './alerts'; +import { requireUIRoutes } from './routes'; +import { EndpointTypes, Globals } from './static_globals'; +import { registerMonitoringTelemetryCollection } from './telemetry_collection'; import { + IBulkUploader, + LegacyRequest, + LegacyShimDependencies, MonitoringCore, MonitoringLicenseService, MonitoringPluginSetup, - LegacyShimDependencies, - IBulkUploader, PluginsSetup, PluginsStart, - LegacyRequest, RequestHandlerContextMonitoringPlugin, } from './types'; -import { Globals, EndpointTypes } from './static_globals'; -import { instantiateClient } from './es_client/instantiate_client'; - // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; @@ -126,7 +125,7 @@ export class MonitoringPlugin const alerts = AlertsFactory.getAll(); for (const alert of alerts) { - plugins.alerting?.registerType(alert.getAlertType()); + plugins.alerting?.registerType(alert.getRuleType()); } const config = createConfig(this.initializerContext.config.get>()); @@ -192,7 +191,11 @@ export class MonitoringPlugin plugins ); - requireUIRoutes(this.monitoringCore, { + if (config.ui.debug_mode) { + this.log.info('MONITORING DEBUG MODE: ON'); + } + + requireUIRoutes(this.monitoringCore, config, { cluster, router, licenseService: this.licenseService, @@ -272,7 +275,7 @@ export class MonitoringPlugin app: ['monitoring', 'kibana'], catalogue: ['monitoring'], privileges: null, - alerting: ALERTS, + alerting: RULES, reserved: { description: i18n.translate('xpack.monitoring.feature.reserved.description', { defaultMessage: 'To grant users access, you should also assign the monitoring_user role.', @@ -289,10 +292,10 @@ export class MonitoringPlugin }, alerting: { rule: { - all: ALERTS, + all: RULES, }, alert: { - all: ALERTS, + all: RULES, }, }, ui: [], diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts index 0f65fde1b296..05a8de96b4c0 100644 --- a/x-pack/plugins/monitoring/server/routes/index.ts +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -4,14 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - /* eslint import/namespace: ['error', { allowComputed: true }]*/ + +import { MonitoringConfig } from '../config'; +import { decorateDebugServer } from '../debug_logger'; +import { RouteDependencies } from '../types'; // @ts-ignore import * as uiRoutes from './api/v1/ui'; // namespace import -import { RouteDependencies } from '../types'; -export function requireUIRoutes(server: any, npRoute: RouteDependencies) { +export function requireUIRoutes( + _server: any, + config: MonitoringConfig, + npRoute: RouteDependencies +) { const routes = Object.keys(uiRoutes); + const server = config.ui.debug_mode + ? decorateDebugServer(_server, config, npRoute.logger) + : _server; routes.forEach((route) => { const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index d0fb7e1a88dc..756b8528865c 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index b794f9123150..4273252850da 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,8 +6,16 @@ }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "observability"], - "optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"], + "configPath": [ + "xpack", + "observability" + ], + "optionalPlugins": [ + "home", + "lens", + "licensing", + "usageCollection" + ], "requiredPlugins": [ "alerting", "cases", diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx deleted file mode 100644 index 0e17c6277618..000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx +++ /dev/null @@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function MobileAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { - defaultMessage: 'Add Mobile data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx deleted file mode 100644 index af91624769e6..000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ /dev/null @@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function SyntheticsAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { - defaultMessage: 'Add synthetics data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx deleted file mode 100644 index c6aa0742466f..000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx +++ /dev/null @@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function UXAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { - defaultMessage: 'Add UX data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx deleted file mode 100644 index 329192abc99d..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ /dev/null @@ -1,59 +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 { render } from '../../rtl_helpers'; -import { fireEvent, screen } from '@testing-library/dom'; -import React from 'react'; -import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; -import * as pluginHook from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ExpViewActionMenuContent } from './action_menu'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('Action Menu', function () { - it('should be able to click open in lens', async function () { - const { findByText, core } = render( - - ); - - expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); - - fireEvent.click(await findByText('Open in Lens')); - - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - id: '', - attributes: sampleAttribute, - timeRange: { to: 'now', from: 'now-10m' }, - }, - true - ); - }); - - it('should be able to click save', async function () { - const { findByText } = render( - - ); - - expect(await screen.findByText('Save')).toBeInTheDocument(); - - fireEvent.click(await findByText('Save')); - - expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx deleted file mode 100644 index 38011eb5f8ff..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ /dev/null @@ -1,92 +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, { useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ObservabilityAppServices } from '../../../../../application/types'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; - -export function ExpViewActionMenuContent({ - timeRange, - lensAttributes, -}: { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -}) { - const kServices = useKibana().services; - - const { lens } = kServices; - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - return ( - <> - - - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - true - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - - - - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - size="s" - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} - - - - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - // if we want to do anything after the viz is saved - // right now there is no action, so an empty function - onSave={() => {}} - /> - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx deleted file mode 100644 index 23500b63e900..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx +++ /dev/null @@ -1,26 +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 { ExpViewActionMenuContent } from './action_menu'; -import HeaderMenuPortal from '../../../header_menu_portal'; -import { usePluginContext } from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; - -interface Props { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} -export function ExpViewActionMenu(props: Props) { - const { appMountParameters } = usePluginContext(); - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index d17e451ef702..3566835b1701 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_editor/series_editor'; -import { ReportViewType, SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_builder/series_builder'; +import { SeriesUrl } from '../types'; export function EmptyView({ loading, + height, series, - reportType, }: { loading: boolean; - series?: SeriesUrl; - reportType: ReportViewType; + height: string; + series: SeriesUrl; }) { - const { dataType, reportDefinitions } = series ?? {}; + const { dataType, reportType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` text-align: center; + height: ${(props) => props.height}; position: relative; `; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index 03fd23631f75..fe2953edd36d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,10 +27,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -52,10 +51,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -76,10 +74,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -103,10 +100,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index c6254a85de9a..a08e777c5ea7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -9,24 +9,21 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; -import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string | string[]; - seriesId: number; - series: SeriesUrl; + value: string; + seriesId: string; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; } export function FilterLabel({ label, seriesId, - series, field, value, negate, @@ -34,7 +31,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId, series }); + const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( { - setSeries(seriesId, { ...series, color: colorN }); - }; - - const color = - series.color ?? ((theme.eui as unknown) as Record)[`euiColorVis${seriesId}`]; - - const button = ( - - setIsOpen((prevState) => !prevState)} hasArrow={false}> - - - - ); - - return ( - setIsOpen(false)}> - - - - - ); -} - -const PICK_A_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.pickColor', - { - defaultMessage: 'Pick a color', - } -); - -const EDIT_SERIES_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.editSeriesColor', - { - defaultMessage: 'Edit color for series', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx deleted file mode 100644 index 23d6589fecbc..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx +++ /dev/null @@ -1,109 +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 moment from 'moment'; -import { EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { useHasData } from '../../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; -import { parseTimeParts } from '../../series_viewer/columns/utils'; -import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; - -export interface TimePickerTime { - from: string; - to: string; -} - -export interface TimePickerQuickRange extends TimePickerTime { - display: string; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - readonly?: boolean; -} -const readableUnit: Record = { - m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', { - defaultMessage: 'Minutes', - }), - h: i18n.translate('xpack.observability.overview.exploratoryView.hour', { - defaultMessage: 'Hour', - }), - d: i18n.translate('xpack.observability.overview.exploratoryView.day', { - defaultMessage: 'Day', - }), -}; - -export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) { - const { onRefreshTimeRange } = useHasData(); - - const commonlyUsedRanges = useQuickTimeRanges(); - - const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage(); - - function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); - if (reportType === ReportTypes.KPI) { - allSeries.forEach((currSeries, seriesIndex) => { - setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); - }); - } else { - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - } - - const seriesTime = series.time ?? firstSeries!.time; - - const dateFormat = useUiSetting('dateFormat').replace('ss.SSS', 'ss'); - - if (readonly) { - const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to); - - if (timeParts) { - const { - timeTense: timeTenseDefault, - timeUnits: timeUnitsDefault, - timeValue: timeValueDefault, - } = timeParts; - - return ( - {`${timeTenseDefault} ${timeValueDefault} ${ - readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault - }`} - ); - } else { - return ( - - {i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', { - defaultMessage: '{start} to {end}', - values: { - start: moment(seriesTime.from).format(dateFormat), - end: moment(seriesTime.to).format(dateFormat), - }, - })} - - ); - } - } - - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index bf5feb7d5863..ba1f2214223e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -94,19 +94,6 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; -export enum ReportTypes { - KPI = 'kpi-over-time', - DISTRIBUTION = 'data-distribution', - CORE_WEB_VITAL = 'core-web-vitals', - DEVICE_DISTRIBUTION = 'device-data-distribution', -} - -export enum DataTypes { - SYNTHETICS = 'synthetics', - UX = 'ux', - MOBILE = 'mobile', -} - export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 55ac75b47c05..6f990015fbc6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,12 +8,10 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', + REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', - HIDDEN = 'h', - NAME = 'n', - COLOR = 'c', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 3f6551986527..574a9f6a2bc1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -15,7 +15,6 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; -import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -25,24 +24,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case DataTypes.UX: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'ux': + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.CORE_WEB_VITAL) { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case DataTypes.SYNTHETICS: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'synthetics': + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case DataTypes.MOBILE: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'mobile': + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 08d2da4714e4..ae70bbdcfa3b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,9 +38,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -53,7 +50,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -66,9 +63,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: RECORDS_FIELD, }, ]); @@ -141,9 +135,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -286,7 +277,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -392,7 +383,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -412,9 +403,6 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -434,7 +422,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -495,7 +483,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -601,9 +589,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5426d3bcd423..dfb17ee470d3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; - import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -37,11 +36,10 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, - ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../components/date_range_picker'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -75,6 +73,14 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -89,9 +95,7 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField: string; - color: string; - name: string; + selectedMetricField?: string; } export class LensAttributes { @@ -467,15 +471,14 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time, + time: { from, to }, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - - if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { + if (reportType !== 'kpi-over-time' && totalLayers > 1) { // for kpi over time, we don't need to add time range filters // since those are essentially plotted along the x-axis - baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`; + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -531,11 +534,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if ( - index === 0 || - mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || - !layerConfig.time - ) { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -547,14 +546,11 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); if (inDays > 1) { return inDays + 'd'; } - const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); - if (inHours === 0) { - return null; - } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); return inHours + 'h'; } @@ -572,12 +568,6 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; - let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; - - if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) { - label = layerConfig.name; - } - layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -591,7 +581,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -634,7 +624,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -648,7 +638,7 @@ export class LensAttributes { }; } - getJSON(refresh?: number): TypedLensByValueInput['attributes'] { + getJSON(): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -657,7 +647,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: String(refresh), + description: '', visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 4e178bba7e02..d1612a08f555 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DEVICE_DISTRIBUTION, + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - definitionFields: [SERVICE_NAME], metricOptions: [ { - field: 'labels.device_id', id: 'labels.device_id', + field: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 1da27be4fcc9..9b1c4c8da3e9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 3ee5b3125fcd..945a631078a3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -32,7 +26,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 35e094996f6f..07bb13f957e4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,13 +24,10 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, - color: 'green', - name: 'test-series', - breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - time: { from: 'now-15m', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - selectedMetricField: LCP_FIELD, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e8d620388a89..62455df24808 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,7 +11,6 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, - ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -39,7 +38,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: ReportTypes.CORE_WEB_VITAL, + reportType: 'core-web-vitals', seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -154,6 +153,5 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], - query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index de6f2c67b2ae..f34c8db6c197 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -46,7 +41,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 9112778eadaa..5899b16d12b4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -49,7 +43,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d1520..730e742f9d8c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -35,7 +30,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 65b43a83a8fb..4ee22181d433 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index a5898f33e0ec..569d68ad4ebf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttribute = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -34,23 +28,17 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - dataType: 'number', - isBucketed: true, + sourceField: 'transaction.duration.us', label: 'Page load time', + dataType: 'number', operationType: 'range', + isBucketed: true, + scale: 'interval', params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', }, - scale: 'interval', - sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -60,7 +48,7 @@ export const sampleAttribute = { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -93,16 +81,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - filter: { - language: 'kuery', - query: - 'transaction.type: page-load and processor.event: transaction and transaction.type : *', - }, isBucketed: false, label: 'Part of count() / overall_sum(count())', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, 'y-axis-column-layer0X2': { customLabel: true, @@ -153,51 +141,26 @@ export const sampleAttribute = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: 'transaction.duration.us < 60000000', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 425bf069cc87..2087b85b8188 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: 'undefined', + description: '', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: 'transaction.type: "page-load"', + query: '', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 85bafdecabde..7f066caf66bf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttributeKpi = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -26,27 +20,25 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { + sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { - interval: 'auto', - }, + params: { interval: 'auto' }, scale: 'interval', - sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', - filter: { - language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', - }, isBucketed: false, - label: 'test-series', + label: 'Page views', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, }, }, incompleteColumns: {}, @@ -54,51 +46,26 @@ export const sampleAttributeKpi = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: '', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: '', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 694250e5749c..f7df2939d990 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,43 +16,40 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, + reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, - hidden, - name, - color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, + [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, - [URL_KEYS.HIDDEN]: hidden, - [URL_KEYS.NAME]: name, - [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl( - { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, - baseHref = '' -) { - const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); return ( baseHref + - `/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode( - (allShortSeries as unknown) as RisonValue - )}` + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 21c749258beb..989ebf17c206 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -11,13 +11,6 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; import * as obsvInd from './utils/observability_index_patterns'; -import * as pluginHook from '../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -48,18 +41,29 @@ describe('ExploratoryView', () => { it('renders exploratory view', async () => { render(); - expect(await screen.findByText(/Preview/i)).toBeInTheDocument(); - expect(await screen.findByText(/Configure series/i)).toBeInTheDocument(); - expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - render(); + const initSeries = { + data: { + 'ux-series': { + isNew: true, + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + breakdown: 'user_agent .name', + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index cb901b8b588f..af04108c5679 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { useRouteMatch } from 'react-router-dom'; -import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -18,15 +16,40 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesViews } from './views/series_views'; +import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export type PanelId = 'seriesPanel' | 'chartPanel'; +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -46,19 +69,20 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${headerOffset + 40}px)`); + const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; + setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); } }; useEffect(() => { - allSeries.forEach((seriesT) => { + Object.values(allSeries).forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -72,104 +96,38 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); - const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); - - const [hiddenPanel, setHiddenPanel] = useState(''); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const onCollapse = (panelId: string) => { - setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); - }; - - const onChange = (panelId: PanelId) => { - onCollapse(panelId); - if (collapseFn.current) { - collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); - } - }; - return ( {lens ? ( <> - + - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - - {lensAttributes ? ( - - ) : ( - - )} - - - - {!isPreview && - (hiddenPanel === 'chartPanel' ? ( - onChange('chartPanel')} iconType="arrowDown"> - {SHOW_CHART_LABEL} - - ) : ( - onChange('chartPanel')} - iconType="arrowUp" - color="text" - > - {HIDE_CHART_LABEL} - - ))} - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - + {lensAttributes ? ( + + ) : ( + )} + ) : ( -

{LENS_NOT_AVAILABLE}

+

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

)}
@@ -189,39 +147,4 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; - position: relative; -`; - -const ShowPreview = styled(EuiButtonEmpty)` - position: absolute; - bottom: 34px; -`; -const HideChart = styled(EuiButtonEmpty)` - position: absolute; - top: -35px; - right: 50px; `; -const ShowChart = styled(EuiButtonEmpty)` - position: absolute; - top: -10px; - right: 50px; -`; - -const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { - defaultMessage: 'Hide chart', -}); - -const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { - defaultMessage: 'Show chart', -}); - -const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', -}); - -const LENS_NOT_AVAILABLE = i18n.translate( - 'xpack.observability.overview.exploratoryView.lensDisabled', - { - defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1f910b946deb..8cd8977fcf74 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -8,22 +8,51 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); +import { fireEvent } from '@testing-library/dom'; describe('ExploratoryViewHeader', function () { it('should render properly', function () { const { getByText } = render( ); - getByText('Refresh'); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + const { getByText, core } = render( + , + { initSeries } + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }, + true + ); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bec8673f88b4..ded56ec9e817 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -5,37 +5,43 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { DataViewLabels } from '../configurations/constants'; +import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { LastUpdated } from './last_updated'; -import { combineTimeRanges } from '../lens_embeddable'; -import { ExpViewActionMenu } from '../components/action_menu'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { - seriesId?: number; - lastUpdated?: number; + seriesId: string; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { - const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const kServices = useKibana().services; - const series = seriesId ? getSeries(seriesId) : undefined; + const { lens } = kServices; - const timeRange = combineTimeRanges(reportType, allSeries, series); + const { getSeries, allSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + const timeRange = combineTimeRanges(allSeries, series); return ( <> -

- {DataViewLabels[reportType] ?? + {DataViewLabels[series.reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -51,18 +57,53 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: - + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + true + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + - setLastRefresh(Date.now())}> - {REFRESH_LABEL} + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + onSave={() => {}} + /> + )} ); } - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index d65917093d12..7a5f12a72b1f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -export type IndexPatternState = Record; +type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx deleted file mode 100644 index e86144c12494..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx +++ /dev/null @@ -1,92 +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 { useCallback, useEffect, useState } from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from './use_app_index_pattern'; -import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; -import { getFiltersFromDefs } from './use_lens_attributes'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface UseDiscoverLink { - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { - const kServices = useKibana().services; - const { - application: { navigateToUrl }, - } = kServices; - - const { indexPatterns } = useAppIndexPatternContext(); - - const urlGenerator = kServices.discover?.urlGenerator; - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const indexPattern = indexPatterns?.[series.dataType]; - - const definitions = series.reportDefinitions ?? {}; - const filters = [...(seriesConfig?.baseFilters ?? [])]; - - const definitionFilters = getFiltersFromDefs(definitions); - - definitionFilters.forEach(({ field, values = [] }) => { - if (values.length > 1) { - filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); - } else { - filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); - } - }); - - const selectedMetricField = series.selectedMetricField; - - if ( - selectedMetricField && - selectedMetricField !== RECORDS_FIELD && - selectedMetricField !== RECORDS_PERCENTAGE_FIELD - ) { - filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); - } - - const getDiscoverUrl = async () => { - if (!urlGenerator?.createUrl) return; - - const newUrl = await urlGenerator.createUrl({ - filters, - indexPatternId: indexPattern?.id, - }); - setDiscoverUrl(newUrl); - }; - getDiscoverUrl(); - }, [ - indexPatterns, - series.dataType, - series.reportDefinitions, - series.selectedMetricField, - seriesConfig?.baseFilters, - urlGenerator, - ]); - - const onClick = useCallback( - (event: React.MouseEvent) => { - if (discoverUrl) { - event.preventDefault(); - - return navigateToUrl(discoverUrl); - } - }, - [discoverUrl, navigateToUrl] - ); - - return { - href: discoverUrl, - onClick, - }; -}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 71945734eeab..8bb265b4f6d8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -9,18 +9,12 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { - AllSeries, - allSeriesKey, - convertAllShortSeries, - useSeriesStorage, -} from './use_series_storage'; +import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; -import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -34,56 +28,41 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { allSeriesIds, allSeries } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); - const theme = useTheme(); - return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const allSeriesT: AllSeries = autoApply - ? allSeries - : convertAllShortSeries(storage.get(allSeriesKey) ?? []); - const layerConfigs: LayerConfig[] = []; - allSeriesT.forEach((series, seriesIndex) => { - const indexPattern = indexPatterns?.[series?.dataType]; - - if ( - indexPattern && - !isEmpty(series.reportDefinitions) && - !series.hidden && - series.selectedMetricField - ) { + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { const seriesConfig = getDefaultConfigs({ - reportType, + reportType: seriesT.reportType, + dataType: seriesT.dataType, indexPattern, - dataType: series.dataType, }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(series.reportDefinitions) + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions) ); - const color = `euiColorVis${seriesIndex}`; - layerConfigs.push({ filters, indexPattern, seriesConfig, - time: series.time, - name: series.name, - breakdown: series.breakdown, - seriesType: series.seriesType, - operationType: series.operationType, - reportDefinitions: series.reportDefinitions ?? {}, - selectedMetricField: series.selectedMetricField, - color: series.color ?? ((theme.eui as unknown) as Record)[color], + time: seriesT.time, + breakdown: seriesT.breakdown, + seriesType: seriesT.seriesType, + operationType: seriesT.operationType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + selectedMetricField: seriesT.selectedMetricField, }); } }); @@ -94,6 +73,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(lastRefresh); - }, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]); + return lensAttributes.getJSON(); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index f2a6130cdc59..2d2618bc4615 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,16 +6,18 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { SeriesUrl, UrlFilter } from '../types'; +import { UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string | string[]; + value: string; negate?: boolean; } -export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { - const { setSeries } = useSeriesStorage(); +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const filters = series.filters ?? []; @@ -24,14 +26,10 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValuesN = filter.notValues?.filter((val) => val !== value); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const valuesN = filter.values?.filter((val) => val !== value); return { ...filter, values: valuesN }; } } @@ -45,9 +43,9 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = value instanceof Array ? value : [value]; + currFilter.notValues = [value]; } else { - currFilter.values = value instanceof Array ? value : [value]; + currFilter.values = [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -67,26 +65,13 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); - - const values = currValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); if (negate) { - if (value instanceof Array) { - notValues.push(...value); - } else { - notValues.push(value); - } + notValues.push(value); } else { - if (value instanceof Array) { - values.push(...value); - } else { - values.push(value); - } + values.push(value); } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ce6d7bd94d8e..c32acc47abd1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,39 +6,37 @@ */ import React, { useEffect } from 'react'; -import { Route, Router } from 'react-router-dom'; -import { render } from '@testing-library/react'; + import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; -import { getHistoryFromUrl } from '../rtl_helpers'; +import { render } from '@testing-library/react'; -const mockSingleSeries = [ - { - name: 'performance-distribution', +const mockSingleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -const mockMultipleSeries = [ - { - name: 'performance-distribution', +const mockMultipleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -describe('userSeriesStorage', function () { +describe('userSeries', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); - function TestComponent() { const data = useSeriesStorage(); @@ -50,20 +48,11 @@ describe('userSeriesStorage', function () { } render( - - - (key === 'sr' ? seriesData : null)), - set: jest.fn(), - }} - > - - - - + + + ); return setData; @@ -74,20 +63,22 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); @@ -98,38 +89,42 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution', 'kpi-over-time'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent([]); + const setData = setupTestComponent({}); - expect(setData).toHaveBeenCalledTimes(1); + expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [], + allSeries: {}, + allSeriesIds: [], firstSeries: undefined, + firstSeriesId: undefined, }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 04f8751e2a0b..a47a124d14b4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -6,7 +6,6 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { IKbnUrlStateStorage, ISessionStorageStateStorage, @@ -23,19 +22,13 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries?: SeriesUrl; - autoApply: boolean; - lastRefresh: number; - setLastRefresh: (val: number) => void; - setAutoApply: (val: boolean) => void; - applyChanges: () => void; + firstSeries: SeriesUrl; + firstSeriesId: string; + allSeriesIds: string[]; allSeries: AllSeries; - setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; - getSeries: (seriesIndex: number) => SeriesUrl | undefined; - removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; - storage: IKbnUrlStateStorage | ISessionStorageStateStorage; - reportType: ReportViewType; + setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; + getSeries: (seriesId: string) => SeriesUrl; + removeSeries: (seriesId: string) => void; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -43,112 +36,72 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -export function convertAllShortSeries(allShortSeries: AllShortSeries) { - return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); -} +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); -export const allSeriesKey = 'sr'; -const autoApplyKey = 'autoApply'; -const reportTypeKey = 'reportType'; + return allSeriesN; +} export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? []) - ); - - const [autoApply, setAutoApply] = useState(() => storage.get(autoApplyKey) ?? true); - const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + const allSeriesKey = 'sr'; - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [allShortSeries, setAllShortSeries] = useState( + () => storage.get(allSeriesKey) ?? {} ); - + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); + const [firstSeriesId, setFirstSeriesId] = useState(''); const [firstSeries, setFirstSeries] = useState(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - useEffect(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - const firstSeriesT = allSeries?.[0]; - - setFirstSeries(firstSeriesT); - - if (autoApply) { - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - } - }, [allSeries, autoApply, storage]); useEffect(() => { - // needed for tab change - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + setAllSeries(allSeriesN); + setFirstSeriesId(allSeriesIds?.[0]); + setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - // this is only needed for tab change, so we will not add allSeries into dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPreview, storage]); - - const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { - setAllSeries((prevAllSeries) => { - const newStateRest = prevAllSeries.map((series, index) => { - if (index === seriesIndex) { - return newValue; - } - return series; - }); - - if (prevAllSeries.length === seriesIndex) { - return [...newStateRest, newValue]; - } - - return [...newStateRest]; + }, [allShortSeries, storage]); + + const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { + setAllShortSeries((prevState) => { + prevState[seriesIdN] = convertToShortUrl(newValue); + return { ...prevState }; }); - }, []); + }; - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); + const removeSeries = (seriesIdN: string) => { + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); + }; - const removeSeries = useCallback((seriesIndex: number) => { - setAllSeries((prevAllSeries) => - prevAllSeries.filter((seriesT, index) => index !== seriesIndex) - ); - }, []); + const allSeriesIds = Object.keys(allShortSeries); const getSeries = useCallback( - (seriesIndex: number) => { - return allSeries[seriesIndex]; + (seriesId?: string) => { + return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); }, [allSeries] ); - const applyChanges = useCallback(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - setLastRefresh(Date.now()); - }, [allSeries, storage]); - - useEffect(() => { - (storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply); - }, [autoApply, storage]); - const value = { - autoApply, - setAutoApply, - applyChanges, storage, getSeries, setSeries, removeSeries, + firstSeriesId, allSeries, - lastRefresh, - setLastRefresh, - setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + allSeriesIds, firstSeries: firstSeries!, }; return {children}; @@ -159,9 +112,10 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, + reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -169,31 +123,26 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, - hidden: h, - name: n, - color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; - [URL_KEYS.HIDDEN]?: boolean; - [URL_KEYS.NAME]: string; - [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = ShortUrlSeries[]; -export type AllSeries = SeriesUrl[]; +export type AllShortSeries = Record; +export type AllSeries = Record; -export const NEW_SERIES_KEY = 'new-series'; +export const NEW_SERIES_KEY = 'new-series-key'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e..e55752ceb62b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 9e4d9486dc15..4cb586fe94ce 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,51 +7,16 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { combineTimeRanges } from './exploratory_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ReportViewType, SeriesUrl } from './types'; -import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } -export const combineTimeRanges = ( - reportType: ReportViewType, - allSeries: SeriesUrl[], - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - - if (reportType === ReportTypes.KPI) { - return firstSeries?.time; - } - - allSeries.forEach((series) => { - if ( - series.dataType && - series.selectedMetricField && - !isEmpty(series.reportDefinitions) && - series.time - ) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - - return { to, from }; -}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -62,11 +27,9 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); + const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); - const firstSeriesId = 0; - - const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; + const timeRange = combineTimeRanges(allSeries, series); const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -74,9 +37,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (reportType !== 'data-distribution' && firstSeries) { + if (series?.reportType !== 'data-distribution') { setSeries(firstSeriesId, { - ...firstSeries, + ...series, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -90,30 +53,16 @@ export function LensEmbeddable(props: Props) { ); } }, - [reportType, setSeries, firstSeries, notifications?.toasts] + [notifications?.toasts, series, firstSeriesId, setSeries] ); - if (timeRange === null || !firstSeries) { - return null; - } - return ( - - - + ); } - -const LensWrapper = styled.div` - height: 100%; - - &&& > div { - height: 100%; - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 0e609cbe6c9e..972e3beb4b72 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Route, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -39,10 +39,9 @@ import { IndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { AppDataType, SeriesUrl, UrlFilter } from './types'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; -import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; interface KibanaProps { services?: KibanaServices; @@ -159,11 +158,9 @@ export function MockRouter({ }: MockRouterProps) { return ( - - - {children} - - + + {children} + ); } @@ -176,7 +173,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url = '/app/observability/exploratory-view/configure#?autoApply=!t', + url, initSeries = {}, }: RenderRouterOptions = {} ) { @@ -206,7 +203,7 @@ export function render( }; } -export const getHistoryFromUrl = (url: Url) => { +const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -255,15 +252,6 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; -export const mockUxSeries = { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - reportDefinitions: { 'service.name': ['elastic-co'] }, - selectedMetricField: TRANSACTION_DURATION, -} as SeriesUrl; - function mockSeriesStorageContext({ data, filters, @@ -273,34 +261,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const testSeries = { - ...mockUxSeries, - breakdown: breakdown || 'user_agent.name', - ...(filters ? { filters } : {}), + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'data-distribution', + dataType: 'ux', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; - const mockDataSeries = data || [testSeries]; + const series = mockDataSeries[firstSeriesId]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(testSeries); + const getSeries = jest.fn().mockReturnValue(series); return { + firstSeriesId, + allSeriesIds, removeSeries, setSeries, getSeries, - autoApply: true, - reportType: 'data-distribution', - lastRefresh: Date.now(), - setLastRefresh: jest.fn(), - setAutoApply: jest.fn(), - applyChanges: jest.fn(), - firstSeries: mockDataSeries[0], + firstSeries: mockDataSeries[firstSeriesId], allSeries: mockDataSeries, - setReportType: jest.fn(), - storage: { get: jest.fn() } as any, - } as SeriesContextValue; + }; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx index 8f196b8a05dd..c054853d9c87 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 27d846502dbe..50c2f91e6067 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; +import { useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,14 +20,16 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - series, + seriesTypes, defaultChartType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; + seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; @@ -40,15 +42,17 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={[ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ]} + includeChartTypes={ + seriesTypes || [ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ] + } label={CHART_TYPE_LABEL} /> ); @@ -101,14 +105,14 @@ export function XYChartTypesSelect({ }); return ( - - - + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx new file mode 100644 index 000000000000..b10702ebded5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; + +describe('DataTypesCol', function () { + const seriesId = 'test-series-id'; + + mockAppIndexPattern(); + + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = render(); + + fireEvent.click(screen.getByText(/user experience \(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); + + it('should set series on change on already selected', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx new file mode 100644 index 000000000000..f386f62d9ed7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'ux', label: 'User Experience (RUM)' }, + { id: 'mobile', label: 'Mobile Experience' }, + // { id: 'infra_logs', label: 'Logs' }, + // { id: 'infra_metrics', label: 'Metrics' }, + // { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol({ seriesId }: { seriesId: string }) { + const { getSeries, setSeries, removeSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { loading } = useAppIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (!dataType) { + removeSeries(seriesId); + } else { + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); + } + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + + + ))} + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx new file mode 100644 index 000000000000..6be78084ae19 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + + return ( + + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} + + ); +} + +const Wrapper = styled.div` + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0px; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index ced4d3af057f..516f04e3812b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -7,66 +7,62 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'performance-distribution': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - render(, { - initSeries, - }); + render(, { initSeries }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'series-id': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - const { setSeries } = render(, { - initSeries, - }); + const { setSeries } = render(, { initSeries }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx index 4c10c9311704..fce1383f30f3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -11,18 +11,17 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; -import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, - series, defaultOperationType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; defaultOperationType?: OperationType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const operationType = series?.operationType; @@ -84,7 +83,11 @@ export function OperationTypeSelect({ return ( ); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: USER_AGENT_OS, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: undefined, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 000000000000..fa2d01691ce1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { SeriesConfig } from '../../types'; + +export function ReportBreakdowns({ + seriesId, + seriesConfig, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx similarity index 65% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 544a294e021e..3d156e0ee9c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, - mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 0; + const seriesId = 'test-series-id'; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,24 +27,36 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); + const initSeries = { + data: { + [seriesId]: { + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + }, + }, + }; + mockUseValuesList([{ label: 'elastic-co', count: 10 }]); - it('renders', async () => { - render( - - ); + it('should render properly', async function () { + render(, { + initSeries, + }); await waitFor(() => { - expect(screen.getByText('Web Application')).toBeInTheDocument(); - expect(screen.getByText('Environment')).toBeInTheDocument(); - expect(screen.getByText('Search Environment')).toBeInTheDocument(); + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); }); }); it('should render selected report definitions', async function () { - render( - - ); + render(, { + initSeries, + }); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -53,7 +65,8 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - + , + { initSeries } ); expect( @@ -67,14 +80,11 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', - name: 'performance-distribution', - breakdown: 'user_agent.name', reportDefinitions: {}, - selectedMetricField: 'transaction.duration.us', - time: { from: 'now-15m', to: 'now' }, + reportType: 'data-distribution', + time: { from: 'now-30d', to: 'now' }, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx new file mode 100644 index 000000000000..0c620abf56e8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import styled from 'styled-components'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; +import { OperationTypeSelect } from './operation_type_select'; +import { DatePickerCol } from './date_picker_col'; +import { parseCustomFieldName } from '../../configurations/lens_attributes'; +import { ReportDefinitionField } from './report_definition_field'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +export function ReportDefinitionCol({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; + + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( + + + + + + {definitionFields.map((field) => ( + + + + ))} + {metricOptions && ( + + + + )} + {(hasOperationType || columnType === 'operation') && ( + + + + )} + + + + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 3651b4b7f075..8a83b5c2a8cb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -6,25 +6,30 @@ */ import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -59,26 +64,23 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); - if (!indexPattern) { - return null; - } - return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - asCombobox={true} - allowExclusions={false} - allowAllValuesSelection={true} - usePrependLabel={false} - compressed={false} - required={isEmpty(selectedReportDefinitions)} - /> + + + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + allowAllValuesSelection={true} + /> + )} + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx new file mode 100644 index 000000000000..0b183b5f20c0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, render } from '../../rtl_helpers'; + +describe('Series Builder ReportFilters', function () { + const seriesId = 'test-series-id'; + + const dataViewSeries = getDefaultConfigs({ + reportType: 'data-distribution', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 000000000000..d5938c5387e8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { SeriesConfig } from '../../types'; + +export function ReportFilters({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx new file mode 100644 index 000000000000..12ae8560453c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; +import { ReportTypes } from '../series_builder'; +import { DEFAULT_TIME } from '../../configurations/constants'; + +describe('ReportTypesCol', function () { + const seriesId = 'performance-distribution'; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = render( + + ); + + fireEvent.click(screen.getByText(/KPI over time/i)); + + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + selectedMetricField: undefined, + reportType: 'kpi-over-time', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + isNew: true, + }, + }, + }; + + const { setSeries } = render( + , + { initSeries } + ); + + const button = screen.getByRole('button', { + name: /KPI over time/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'synthetics', + time: DEFAULT_TIME, + isNew: true, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx new file mode 100644 index 000000000000..c4eebbfaca3e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,108 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; +import { ReportViewType, SeriesUrl } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { DEFAULT_TIME } from '../../configurations/constants'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem } from '../series_builder'; + +interface Props { + seriesId: string; + reportTypes: ReportTypeItem[]; +} + +export function ReportTypesCol({ seriesId, reportTypes }: Props) { + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); + + const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); + + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); + + if (!restSeries.dataType) { + return ( + + ); + } + + if (!loading && !hasData) { + return ( + + ); + } + + const disabledReportTypes: ReportViewType[] = map( + reportTypes.filter( + ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ reportType, label }) => ( + + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'No data type selected.' } +); + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx index c352ec0423dd..874171de123d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; -import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -19,34 +18,20 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 5000); + }, 1000); return () => { clearInterval(interVal); }; }, []); - useEffect(() => { - setRefresh(Date.now()); - }, [lastUpdated]); - if (!lastUpdated) { return null; } - const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; - const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; - return ( - - - + + Last Updated: {moment(lastUpdated).from(refresh)} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx new file mode 100644 index 000000000000..a2a3e34c2183 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.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 React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig } from '../types'; + +interface Props { + seriesId: string; + defaultValue?: string; + options: SeriesConfig['metricOptions']; +} + +export function ReportMetricOptions({ seriesId, options: opts }: Props) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const onChange = (value: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + const options = opts ?? []; + + return ( + ({ + value: fd || id, + inputDisplay: label, + }))} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} + onChange={(value) => onChange(value)} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 000000000000..684cf3a210a5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,303 @@ +/* + * 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, { RefObject, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesActions } from '../series_editor/columns/series_actions'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LastUpdated } from './last_updated'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../configurations/constants/labels'; + +export interface ReportTypeItem { + id: string; + reportType: ReportViewType; + label: string; +} + +export const ReportTypes: Record = { + synthetics: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + ], + ux: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + ], + mobile: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, + ], + apm: [], + infra_logs: [], + infra_metrics: [], +}; + +interface BuilderItem { + id: string; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesBuilder({ + seriesBuilderRef, + lastUpdated, + multiSeries, +}: { + seriesBuilderRef: RefObject; + lastUpdated?: number; + multiSeries?: boolean; +}) { + const [editorItems, setEditorItems] = useState([]); + const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + indexPattern: indexPatterns[dataType], + reportType: reportType!, + }); + } + }; + + const seriesToEdit: BuilderItem[] = + allSeriesIds + .filter((sId) => { + return allSeries?.[sId]?.isNew; + }) + .map((sId) => { + const series = getSeries(sId); + const seriesConfig = getDataViewSeries(series.dataType, series.reportType); + + return { id: sId, series, seriesConfig }; + }) ?? []; + const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; + setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); + }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + field: 'id', + width: '15%', + render: (seriesId: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '15%', + field: 'id', + render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + field: 'id', + render: ( + seriesId: string, + { series: { dataType, reportType }, seriesConfig }: BuilderItem + ) => { + if (dataType && seriesConfig) { + return loading ? ( + LOADING_VIEW + ) : reportType ? ( + + ) : ( + SELECT_REPORT_TYPE + ); + } + + return null; + }, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + ...(multiSeries + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: BuilderItem) => ( + + ), + }, + ] + : []), + ]; + + const applySeries = () => { + editorItems.forEach(({ series, id: seriesId }) => { + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + + if (reportType && !isEmpty(reportDefinitions)) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + const newSeriesId = `${reportDefId}-${reportType}`; + + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }); + }; + + const addSeries = () => { + const prevSeries = allSeries?.[allSeriesIds?.[0]]; + setSeries( + `${NEW_SERIES_KEY}-${editorItems.length + 1}`, + prevSeries + ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) + : ({ isNew: true } as SeriesUrl) + ); + }; + + return ( + + {multiSeries && ( + + + + + + {}} + compressed + /> + + + applySeries()} isDisabled={true} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + + addSeries()} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add Series', + })} + + + + )} +
+ {multiSeries && } + {editorItems.length > 0 && ( + + )} + +
+
+ ); +} + +const Wrapper = euiStyled.div` + max-height: 50vh; + overflow-y: scroll; + overflow-x: clip; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx index 0b8e1c1785c7..c30863585b3b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { Moment } from 'moment'; import DateMath from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; +import { Moment } from 'moment'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../types'; -import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { - const { firstSeries, setSeries, reportType } = useSeriesStorage(); +export function DateRangePicker({ seriesId }: { seriesId: string }) { + const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const seriesFrom = series.time?.from; - const seriesTo = series.time?.to; + const { + time: { from, to }, + reportType, + } = getSeries(firstSeriesId); - const { from: mainFrom, to: mainTo } = firstSeries!.time; + const series = getSeries(seriesId); - const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; - const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; + const { + time: { from: seriesFrom, to: seriesTo }, + } = series; - const getTotalDuration = () => { - const mainStartDate = parseAbsoluteDate(mainTo)!; - const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; - return mainEndDate.diff(mainStartDate, 'millisecond'); - }; + const startDate = parseAbsoluteDate(seriesFrom ?? from)!; + const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; - const onStartChange = (newStartDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newFrom = newStartDate.toISOString(); - const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newFrom = newDate.toISOString(); + const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newStartDate.toISOString(); + const newFrom = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,19 +55,20 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series }); } }; - - const onEndChange = (newEndDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newTo = newEndDate.toISOString(); - const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); + const onEndChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newTo = newDate.toISOString(); + const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newEndDate.toISOString(); + const newTo = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -89,7 +90,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } @@ -103,7 +104,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 000000000000..e21da424b58c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.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 { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; +import { DEFAULT_TIME } from '../configurations/constants'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: DEFAULT_TIME }); + } + }, [series, seriesId, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx similarity index 50% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 3517508300e4..931dfbe07cd2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -6,48 +6,67 @@ */ import React from 'react'; -import { mockUseHasData, render } from '../../rtl_helpers'; +import { mockUseHasData, render } from '../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; +import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; - const { getByText } = render(, { - initSeries, - }); + const { getByText } = render(, { initSeries }); - getByText('Last 30 Minutes'); + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + reportType: 'kpi-over-time' as const, + dataType: 'synthetics' as const, + breakdown: 'monitor.status', + }, + }, + }; + const { setSeries: setSeries1 } = render( + , + { initSeries: initSeries as any } + ); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + time: DEFAULT_TIME, + }); }); it('should set series data', async function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render( - , - { - initSeries, - } - ); + const { getByTestId, setSeries } = render(, { + initSeries, + }); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -57,10 +76,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(0, { - name: 'uptime-pings-histogram', + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', + reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx new file mode 100644 index 000000000000..207a53e13f1a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Breakdowns } from './columns/breakdowns'; +import { SeriesConfig } from '../types'; +import { ChartOptions } from './columns/chart_options'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; + breakdownFields: string[]; +} +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 21b766227a56..84568e1c5068 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { mockIndexPattern, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,7 +20,13 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render(); + render( + + ); screen.getAllByText('Browser family'); }); @@ -30,9 +36,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -43,14 +49,10 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - name: 'performance-distribution', - reportDefinitions: { - 'service.name': ['elastic-co'], - }, - selectedMetricField: 'transaction.duration.us', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 315f63e33bed..2237935d466a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useRouteMatch } from 'react-router-dom'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; + breakdowns: string[]; seriesConfig: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, series }: Props) { - const { setSeries } = useSeriesStorage(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { + const { setSeries, getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,13 +40,9 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; - if (!seriesConfig) { - return null; - } - const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = seriesConfig.breakdownFields.map((breakdown) => ({ + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -54,12 +50,14 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: NO_BREAK_DOWN_LABEL, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), }); } const options = items.map(({ id, label }) => ({ - inputDisplay: label, + inputDisplay: id === NO_BREAKDOWN ? label : {label}, value: id, dropdownDisplay: label, })); @@ -71,7 +69,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
onOptionChange(value)} @@ -80,10 +78,3 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
); } - -export const NO_BREAK_DOWN_LABEL = i18n.translate( - 'xpack.observability.exp.breakDownFilter.noBreakdown', - { - defaultMessage: 'No breakdown', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx new file mode 100644 index 000000000000..f2a6377fd9b7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesConfig } from '../../types'; +import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; +import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; +} + +export function ChartOptions({ seriesConfig, seriesId }: Props) { + return ( + + + + + {seriesConfig.hasOperationType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx deleted file mode 100644 index 838631e1f05d..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx +++ /dev/null @@ -1,39 +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 { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; -import { DataTypesLabels, DataTypesSelect } from './data_type_select'; -import { DataTypes } from '../../configurations/constants'; - -describe('DataTypeSelect', function () { - const seriesId = 0; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - }); - - it('should set series on change', async function () { - const { setSeries } = render(); - - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX])); - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - name: 'synthetics-series-1', - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx deleted file mode 100644 index b0a6e3b5e26b..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx +++ /dev/null @@ -1,105 +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 { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { AppDataType, SeriesUrl } from '../../types'; -import { DataTypes, ReportTypes } from '../../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export const DataTypesLabels = { - [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { - defaultMessage: 'User experience (RUM)', - }), - - [DataTypes.SYNTHETICS]: i18n.translate( - 'xpack.observability.overview.exploratoryView.syntheticsLabel', - { - defaultMessage: 'Synthetics monitoring', - } - ), - - [DataTypes.MOBILE]: i18n.translate( - 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', - { - defaultMessage: 'Mobile experience', - } - ), -}; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { - id: DataTypes.SYNTHETICS, - label: DataTypesLabels[DataTypes.SYNTHETICS], - }, - { - id: DataTypes.UX, - label: DataTypesLabels[DataTypes.UX], - }, - { - id: DataTypes.MOBILE, - label: DataTypesLabels[DataTypes.MOBILE], - }, -]; - -const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; - -export function DataTypesSelect({ seriesId, series }: Props) { - const { setSeries, reportType } = useSeriesStorage(); - - const onDataTypeChange = (dataType: AppDataType) => { - if (String(dataType) !== SELECT_DATA_TYPE) { - setSeries(seriesId, { - dataType, - time: series.time, - name: `${dataType}-series-${seriesId + 1}`, - }); - } - }; - - const options = dataTypes - .filter(({ id }) => { - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { - return id === DataTypes.MOBILE; - } - if (reportType === ReportTypes.CORE_WEB_VITAL) { - return id === DataTypes.UX; - } - return true; - }) - .map(({ id, label }) => ({ - value: id, - inputDisplay: label, - })); - - return ( - onDataTypeChange(value as AppDataType)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.selectDataType', - { - defaultMessage: 'Select data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 032eb66dcfa4..41e83f407af2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,84 +6,24 @@ */ import React from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesDatePicker } from '../../series_date_picker'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../components/date_range_picker'; -import { SeriesDatePicker } from '../../components/series_date_picker'; -import { AppDataType, SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; -import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; -import { UXAddData } from '../../../add_data_buttons/ux_add_data'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; } - -const AddDataComponents: Record = { - mobile: MobileAddData, - ux: UXAddData, - synthetics: SyntheticsAddData, - apm: null, - infra_logs: null, - infra_metrics: null, -}; - -export function DatePickerCol({ seriesId, series }: Props) { - const { reportType } = useSeriesStorage(); - - const { hasAppData } = useAppIndexPatternContext(); - - if (!series.dataType) { - return null; - } - - const AddDataButton = AddDataComponents[series.dataType]; - if (hasAppData[series.dataType] === false && AddDataButton !== null) { - return ( - - - - {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { - defaultMessage: 'No {dataType} data available.', - values: { - dataType: series.dataType, - }, - })} - - - - - - - ); - } - - if (!series.selectedMetricField) { - return null; - } +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); return ( - - {seriesId === 0 || reportType !== ReportTypes.KPI ? ( - +
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + ) : ( - + )} - +
); } - -const Wrapper = styled.div` - width: 100%; - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx similarity index 67% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index a88e2eadd10c..90a039f6b44d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -8,24 +8,20 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - - const mockSeries = { ...mockUxSeries, filters }; - - it('render', async () => { - const initSeries = { filters }; + it('should render properly', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockAppIndexPattern(); render( , { initSeries } @@ -37,14 +33,15 @@ describe('FilterExpanded', function () { }); it('should call go back on click', async function () { - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const goBack = jest.fn(); render( , { initSeries } @@ -52,23 +49,28 @@ describe('FilterExpanded', function () { await waitFor(() => { fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); }); }); - it('calls useValuesList on load', async () => { - const initSeries = { filters }; + it('should call useValuesList on load', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); + const goBack = jest.fn(); + render( , { initSeries } @@ -85,8 +87,8 @@ describe('FilterExpanded', function () { }); }); - it('filters display values', async () => { - const initSeries = { filters }; + it('should filter display values', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -95,20 +97,18 @@ describe('FilterExpanded', function () { render( , { initSeries } ); - await waitFor(() => { - fireEvent.click(screen.getByText('Browser Family')); - - expect(screen.queryByText('Firefox')).toBeTruthy(); + expect(screen.getByText('Firefox')).toBeTruthy(); + await waitFor(() => { fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); expect(screen.queryByText('Firefox')).toBeFalsy(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 1ef25722aff5..4310402a43a0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,14 +6,7 @@ */ import React, { useState, Fragment } from 'react'; -import { - EuiFieldSearch, - EuiSpacer, - EuiFilterGroup, - EuiText, - EuiPopover, - EuiFilterButton, -} from '@elastic/eui'; +import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; @@ -21,7 +14,8 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,33 +23,31 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; label: string; field: string; isNegated?: boolean; + goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } -export interface NestedFilterOpen { - value: string; - negate: boolean; -} - export function FilterExpanded({ seriesId, - series, field, label, + goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const queryFilters: ESFilter[] = []; @@ -89,71 +81,62 @@ export function FilterExpanded({ ); return ( - setIsOpen((prevState) => !prevState)} iconType="arrowDown"> - {label} - - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( - - )} + + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( - - - - ))} - - - + )} + + + + + ))} + +
); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx similarity index 64% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 764a27fd663f..a9609abc70d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,98 +19,84 @@ describe('FilterValueButton', function () { render( ); - await waitFor(() => { - expect(screen.getByText('Chrome')).toBeInTheDocument(); - }); + screen.getByText('Chrome'); }); - describe('when negate is true', () => { - it('displays negate stats', async () => { - render( - - ); + it('should render display negate state', async function () { + render( + + ); - await waitFor(() => { - expect(screen.getByText('Not Chrome')).toBeInTheDocument(); - expect(screen.getByTitle('Not Chrome')).toBeInTheDocument(); - const btn = screen.getByRole('button'); - expect(btn.classList).toContain('euiButtonEmpty--danger'); - }); + await waitFor(() => { + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); }); + }); - it('calls setFilter on click', async () => { - const { setFilter, removeFilter } = mockUseSeriesFilter(); + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); - render( - - ); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Not Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledTimes(0); - expect(setFilter).toHaveBeenCalledTimes(1); - - expect(setFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: true, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', }); }); }); - describe('when selected', () => { - it('removes the filter on click', async () => { - const { removeFilter } = mockUseSeriesFilter(); - - render( - - ); + it('should remove filter on click if already selected', async function () { + const { removeFilter } = mockUseSeriesFilter(); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: false, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', }); }); }); @@ -121,13 +107,12 @@ describe('FilterValueButton', function () { render( ); @@ -149,14 +134,13 @@ describe('FilterValueButton', function () { render( ); @@ -183,14 +167,13 @@ describe('FilterValueButton', function () { render( ); @@ -220,14 +203,13 @@ describe('FilterValueButton', function () { render( ); @@ -247,14 +229,13 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 111f915a95f4..bf4ca6eb83d9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { SeriesUrl } from '../../types'; -import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -20,13 +19,12 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: number; - series: SeriesUrl; + seriesId: string; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: NestedFilterOpen) => void; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; } export function FilterValueButton({ @@ -36,13 +34,16 @@ export function FilterValueButton({ field, negate, seriesId, - series, nestedField, allSelectedValues, }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 000000000000..e75f308dab1e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} + +export function RemoveSeries({ seriesId }: Props) { + const { removeSeries } = useSeriesStorage(); + + const onClick = () => { + removeSeries(seriesId); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx deleted file mode 100644 index dad2a7da2367..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ /dev/null @@ -1,59 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { ReportDefinitionField } from './report_definition_field'; - -export function ReportDefinitionCol({ - seriesId, - series, - seriesConfig, -}: { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -}) { - const { setSeries } = useSeriesStorage(); - - const { reportDefinitions: selectedReportDefinitions = {} } = series; - - const { definitionFields } = seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - return ( - - {definitionFields.map((field) => ( - - - - ))} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx deleted file mode 100644 index 01c9fce7637b..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx +++ /dev/null @@ -1,64 +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 { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportViewType } from '../../types'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../../configurations/constants/labels'; - -const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; - -export const reportTypesList: Array<{ - reportType: ReportViewType | typeof SELECT_REPORT_TYPE; - label: string; -}> = [ - { - reportType: SELECT_REPORT_TYPE, - label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { - defaultMessage: 'Select report type', - }), - }, - { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, -]; - -export function ReportTypesSelect() { - const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); - - const onReportTypeChange = (reportType: ReportViewType) => { - setReportType(reportType); - }; - - const options = reportTypesList - .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) - .map(({ reportType, label }) => ({ - value: reportType, - inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, - dropdownDisplay: label, - })); - - return ( - onReportTypeChange(value as ReportViewType)} - style={{ minWidth: 200 }} - isInvalid={!selectedReportType && allSeries.length > 0} - disabled={allSeries.length > 0} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx new file mode 100644 index 000000000000..51ebe6c6bd9d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { RemoveSeries } from './remove_series'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: string; + editorMode?: boolean; +} +export function SeriesActions({ seriesId, editorMode = false }: Props) { + const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); + const series = getSeries(seriesId); + + const onEdit = () => { + setSeries(seriesId, { ...series, isNew: true }); + }; + + const copySeries = () => { + let copySeriesId: string = `${seriesId}-copy`; + if (allSeriesIds.includes(copySeriesId)) { + copySeriesId = copySeriesId + allSeriesIds.length; + } + setSeries(copySeriesId, series); + }; + + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + const isSaveAble = reportType && !isEmpty(reportDefinitions); + + const saveSeries = () => { + if (isSaveAble) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + let newSeriesId = `${reportDefId}-${reportType}`; + + if (allSeriesIds.includes(newSeriesId)) { + newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; + } + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }; + + return ( + + {!editorMode && ( + + + + )} + {editorMode && ( + + + + )} + {editorMode && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 000000000000..02144c6929b3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,155 @@ +/* + * 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'; +import React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { SeriesConfig } from '../../types'; +import { FieldLabels } from '../../configurations/constants/constants'; +import { SelectedFilters } from '../selected_filters'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; + isNew?: boolean; + labels?: Record; +} + +export interface Field { + label: string; + field: string; + nested?: string; + isNegated?: boolean; +} + +export function SeriesFilter({ + seriesConfig, + isNew, + seriesId, + filterFields = [], + baseFilters, + labels, +}: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options: Field[] = filterFields.map((field) => { + if (typeof field === 'string') { + return { label: labels?.[field] ?? FieldLabels[field], field }; + } + + return { + field: field.field, + nested: field.nested, + isNegated: field.isNegated, + label: labels?.[field.field] ?? FieldLabels[field.field], + }; + }); + + const { setSeries, getSeries } = useSeriesStorage(); + const urlSeries = getSeries(seriesId); + + const button = ( + { + setIsPopoverVisible((prevState) => !prevState); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + filters={baseFilters} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx deleted file mode 100644 index 801c885ec9a6..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ /dev/null @@ -1,77 +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 { i18n } from '@kbn/i18n'; - -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { OperationTypeSelect } from './columns/operation_type_select'; -import { parseCustomFieldName } from '../configurations/lens_attributes'; -import { SeriesFilter } from '../series_viewer/columns/series_filter'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) { - if (!seriesConfig) { - return null; - } - - const { selectedMetricField } = series ?? {}; - - const { hasOperationType, yAxisColumns } = seriesConfig; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( -
- - - - - - - - - - - - - {(hasOperationType || columnType === 'operation') && ( - - - - - - )} - - -
- ); -} - -const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { - defaultMessage: 'Filters', -}); - -const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { - defaultMessage: 'Operation', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx deleted file mode 100644 index 85eb85e0fc30..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ /dev/null @@ -1,101 +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 { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; - defaultValue?: string; - metricOptions: SeriesConfig['metricOptions']; -} - -const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC'; - -export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) { - const { setSeries } = useSeriesStorage(); - - const { indexPatterns } = useAppIndexPatternContext(); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - if (!series.dataType) { - return null; - } - - const indexPattern = indexPatterns?.[series.dataType]; - - const options = (metricOptions ?? []).map(({ label, field, id }) => { - let disabled = false; - - if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { - disabled = !Boolean(indexPattern?.getFieldByName(field)); - } - return { - disabled, - value: field || id, - dropdownDisplay: disabled ? ( - {field}, - }} - /> - } - > - {label} - - ) : ( - label - ), - inputDisplay: label, - }; - }); - - return ( - onChange(value)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.selectReportMetric', - { - defaultMessage: 'Select report metric', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 8fc5ae95fd41..eb76772a66c7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; @@ -22,19 +22,11 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - render( - , - { - initSeries, - } - ); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 000000000000..5d2ce6ba8495 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { FilterLabel } from '../components/filter_label'; +import { SeriesConfig, UrlFilter } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + seriesConfig: SeriesConfig; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); + + // we don't want to display report definition filters in new series view + if (isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + indexPattern={indexPattern} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + indexPattern={indexPattern} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + indexPattern={indexPattern} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 80fe40083083..c3cc8484d175 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,399 +5,134 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButtonIcon, - EuiSpacer, - EuiFormRow, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import classNames from 'classnames'; -import { isEmpty } from 'lodash'; -import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; -import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SeriesFilter } from './columns/series_filter'; +import { SeriesConfig } from '../types'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesActions } from '../series_viewer/columns/series_actions'; -import { SeriesInfo } from '../series_viewer/columns/series_info'; -import { DataTypesSelect } from './columns/data_type_select'; import { DatePickerCol } from './columns/date_picker_col'; -import { ExpandedSeriesRow } from './expanded_series_row'; -import { SeriesName } from '../series_viewer/columns/series_name'; -import { ReportTypesSelect } from './columns/report_type_select'; -import { ViewActions } from '../views/view_actions'; -import { ReportMetricOptions } from './report_metric_options'; -import { Breakdowns } from '../series_viewer/columns/breakdowns'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { SeriesActions } from './columns/series_actions'; +import { ChartEditOptions } from './chart_edit_options'; -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export interface BuilderItem { - id: number; - series: SeriesUrl; +interface EditItem { seriesConfig: SeriesConfig; + id: string; } -type ExpandedRowMap = Record; - -export const getSeriesToEdit = ({ - indexPatterns, - allSeries, - reportType, -}: { - allSeries: SeriesContextValue['allSeries']; - indexPatterns: IndexPatternState; - reportType: ReportViewType; -}): BuilderItem[] => { - const getDataViewSeries = (dataType: AppDataType) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - reportType, - indexPattern: indexPatterns[dataType], - }); - } - }; - - return allSeries.map((series, seriesIndex) => { - const seriesConfig = getDataViewSeries(series.dataType)!; - - return { id: seriesIndex, series, seriesConfig }; - }); -}; - -export const SeriesEditor = React.memo(function () { - const [editorItems, setEditorItems] = useState([]); - - const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( - {} - ); - - useEffect(() => { - const newExpandRows: ExpandedRowMap = {}; - - setEditorItems((prevState) => { - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach(({ series, id, seriesConfig }) => { - const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); - if ( - prevSeriesItem && - series.selectedMetricField && - prevSeriesItem.series.selectedMetricField !== series.selectedMetricField - ) { - newExpandRows[id] = ( - - ); - } - }); - return [...newEditorItems]; - }); - - setItemIdToExpandedRowMap((prevState) => { - return { ...prevState, ...newExpandRows }; - }); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - useEffect(() => { - setItemIdToExpandedRowMap((prevState) => { - const itemIdToExpandedRowMapValues = { ...prevState }; - - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach((item) => { - if (itemIdToExpandedRowMapValues[item.id]) { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - }); - return itemIdToExpandedRowMapValues; - }); - }, [allSeries, editorItems, indexPatterns, reportType]); - - const toggleDetails = (item: BuilderItem) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[item.id]) { - delete itemIdToExpandedRowMapValues[item.id]; - } else { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; +export function SeriesEditor() { + const { allSeries, allSeriesIds } = useSeriesStorage(); const columns = [ - { - align: 'left' as const, - width: '40px', - isExpander: true, - field: 'id', - name: '', - render: (id: number, item: BuilderItem) => - item.series.dataType && item.series.selectedMetricField ? ( - toggleDetails(item)} - isDisabled={!item.series.dataType || !item.series.selectedMetricField} - aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null, - }, - { - name: '', - field: 'id', - width: '40px', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - - ), - }, { name: i18n.translate('xpack.observability.expView.seriesEditor.name', { defaultMessage: 'Name', }), field: 'id', - width: '20%', - render: (seriesId: number, { series }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', { - defaultMessage: 'Data type', - }), - field: 'id', width: '15%', - render: (seriesId: number, { series }: BuilderItem) => ( - + render: (seriesId: string) => ( + + {' '} + {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', { - defaultMessage: 'Report metric', + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', }), - field: 'id', + field: 'defaultFilters', width: '15%', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', }), field: 'id', - width: '27%', - render: (seriesId: number, { series }: BuilderItem) => ( - + width: '25%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + ), }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - width: '10%', - field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - + name: ( +
+ +
), + width: '20%', + field: 'id', + align: 'right' as const, + render: (seriesId: string, item: EditItem) => , }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { defaultMessage: 'Actions', }), align: 'center' as const, - width: '8%', + width: '10%', field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - - ), + render: (seriesId: string, item: EditItem) => , }, ]; - const getRowProps = (item: BuilderItem) => { - const { dataType, reportDefinitions, selectedMetricField } = item.series; - - return { - className: classNames({ - isExpanded: itemIdToExpandedRowMap[item.id], - isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField, - }), - // commenting this for now, since adding on click on row, blocks adding space - // into text field for name column - // ...(dataType && selectedMetricField - // ? { - // onClick: (evt: MouseEvent) => { - // const targetElem = evt.target as HTMLElement; - // - // if ( - // targetElem.classList.contains('euiTableCellContent') && - // targetElem.tagName !== 'BUTTON' - // ) { - // toggleDetails(item); - // } - // evt.stopPropagation(); - // evt.preventDefault(); - // }, - // } - // : {}), - }; - }; - - const resetView = () => { - const totalSeries = allSeries.length; - for (let i = totalSeries; i >= 0; i--) { - removeSeries(i); - } - setEditorItems([]); - setItemIdToExpandedRowMap({}); - }; - - return ( - -
- - - - - - - - {reportType && ( - - resetView()} color="text"> - {RESET_LABEL} - - - )} - - - - - - - - {editorItems.length > 0 && ( - - )} - -
-
- ); -}); - -const Wrapper = euiStyled.div` - max-height: 50vh; - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } - - &&& { - .euiTableRow-isExpandedRow .euiTableRowCell { - border-top: none; - background-color: #FFFFFF; - border-bottom: 2px solid #d3dae6; - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - } - - .isExpanded { - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - .euiTableRowCell { - border-bottom: none; - } - } - .isIncomplete .euiTableRowCell { - background-color: rgba(254, 197, 20, 0.1); + const { indexPatterns } = useAppIndexPatternContext(); + const items: EditItem[] = []; + + allSeriesIds.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { + items.push({ + id: seriesKey, + seriesConfig: getDefaultConfigs({ + indexPattern: indexPatterns[series.dataType], + reportType: series.reportType, + dataType: series.dataType, + }), + }); } - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); - -export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { - defaultMessage: 'Reset', -}); + }); -export const REPORT_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesBuilder.reportType', - { - defaultMessage: 'Report type', + if (items.length === 0 && allSeriesIds.length > 0) { + return null; } -); - -const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', { - defaultMessage: 'Collapse', -}); -const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', { - defaultMessage: 'Exapnd', -}); + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx deleted file mode 100644 index e6ba505c8209..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx +++ /dev/null @@ -1,70 +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, { useState } from 'react'; -import { EuiPopover, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - useKibana, - ToolbarButton, -} from '../../../../../../../../../src/plugins/kibana_react/public'; -import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { - const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; - - const { - services: { lens }, - } = useKibana(); - - const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - return ( - setIsPopoverOpen(false)} - button={ - - id === seriesType)?.icon!} - aria-label={CHART_TYPE_LABEL} - onClick={() => setIsPopoverOpen((prevState) => !prevState)} - /> - - } - > - - - ); -} - -const EDIT_CHART_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', - { - defaultMessage: 'Edit chart type for series', - } -); - -const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { - defaultMessage: 'Chart type', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx deleted file mode 100644 index 2d38b81e12c9..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: number; -} - -export function RemoveSeries({ seriesId }: Props) { - const { removeSeries, allSeries } = useSeriesStorage(); - - const onClick = () => { - removeSeries(seriesId); - }; - - const isDisabled = seriesId === 0 && allSeries.length > 1; - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx deleted file mode 100644 index 72ae111f002b..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx +++ /dev/null @@ -1,104 +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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { RemoveSeries } from './remove_series'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useDiscoverLink } from '../../hooks/use_discover_link'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SeriesActions({ seriesId, series, seriesConfig }: Props) { - const { setSeries, allSeries } = useSeriesStorage(); - - const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); - - const copySeries = () => { - let copySeriesId: string = `${series.name}-copy`; - if (allSeries.find(({ name }) => name === copySeriesId)) { - copySeriesId = copySeriesId + allSeries.length; - } - setSeries(allSeries.length, { ...series, name: copySeriesId }); - }; - - const toggleSeries = () => { - if (series.hidden) { - setSeries(seriesId, { ...series, hidden: undefined }); - } else { - setSeries(seriesId, { ...series, hidden: true }); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx deleted file mode 100644 index 87c17d03282c..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx +++ /dev/null @@ -1,69 +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 { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; - -interface Props { - seriesId: number; - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export interface Field { - label: string; - field: string; - nested?: string; - isNegated?: boolean; -} - -export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const options: Field[] = seriesConfig.filterFields.map((field) => { - if (typeof field === 'string') { - return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; - } - - return { - field: field.field, - nested: field.nested, - isNegated: field.isNegated, - label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], - }; - }); - - return ( - <> - {!isPreview && ( - <> - - {options.map((opt) => ( - - ))} - - - - )} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx deleted file mode 100644 index 3506acbeb528..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEmpty } from 'lodash'; -import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesChartTypes } from './chart_types'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesColorPicker } from '../../components/series_color_picker'; -import { dataTypes } from '../../series_editor/columns/data_type_select'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - const { dataType, reportDefinitions, selectedMetricField } = series; - - const { loading } = useAppIndexPatternContext(); - - const isIncomplete = - (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; - - if (!seriesConfig) { - return null; - } - - const { definitionFields, labels } = seriesConfig; - - const incompleteDefinition = isEmpty(reportDefinitions) - ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { - defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, - }) - : ''; - - let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; - - if (!dataType) { - incompleteMessage = MISSING_DATA_TYPE_LABEL; - } - - if (!isIncomplete && seriesConfig && isConfigure) { - return ( - - - - - - - - - ); - } - - return ( - - - {isIncomplete && {incompleteMessage}} - - {!isConfigure && ( - - - {dataTypes.find(({ id }) => id === dataType)!.label} - - - )} - - ); -} - -const MISSING_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingReportMetric', - { - defaultMessage: 'Missing report metric', - } -); - -const MISSING_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingDataType', - { - defaultMessage: 'Missing data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx deleted file mode 100644 index e35966a9fb0d..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, ChangeEvent, useEffect } from 'react'; -import { EuiFieldText } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export function SeriesName({ series, seriesId }: Props) { - const { setSeries } = useSeriesStorage(); - - const [value, setValue] = useState(series.name); - - const onChange = (e: ChangeEvent) => { - setValue(e.target.value); - }; - - const onSave = () => { - if (value !== series.name) { - setSeries(seriesId, { ...series, name: value }); - } - }; - - useEffect(() => { - setValue(series.name); - }, [series.name]); - - return ; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts deleted file mode 100644 index b9ee53a7e8e2..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts +++ /dev/null @@ -1,104 +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 moment from 'moment'; -import dateMath from '@elastic/datemath'; -import _isString from 'lodash/isString'; - -const LAST = 'Last'; -const NEXT = 'Next'; - -const isNow = (value: string) => value === 'now'; - -export const isString = (value: any): value is string => _isString(value); -export interface QuickSelect { - timeTense: string; - timeValue: number; - timeUnits: TimeUnitId; -} -export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; -export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; - -export interface RelativeOption { - text: string; - value: TimeUnitId | TimeUnitFromNowId; -} - -export const relativeOptions: RelativeOption[] = [ - { text: 'Seconds ago', value: 's' }, - { text: 'Minutes ago', value: 'm' }, - { text: 'Hours ago', value: 'h' }, - { text: 'Days ago', value: 'd' }, - { text: 'Weeks ago', value: 'w' }, - { text: 'Months ago', value: 'M' }, - { text: 'Years ago', value: 'y' }, - - { text: 'Seconds from now', value: 's+' }, - { text: 'Minutes from now', value: 'm+' }, - { text: 'Hours from now', value: 'h+' }, - { text: 'Days from now', value: 'd+' }, - { text: 'Weeks from now', value: 'w+' }, - { text: 'Months from now', value: 'M+' }, - { text: 'Years from now', value: 'y+' }, -]; - -const timeUnitIds = relativeOptions - .map(({ value }) => value) - .filter((value) => !value.includes('+')) as TimeUnitId[]; - -export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); - -/** - * This function returns time value, time unit and time tense for a given time string. - * - * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. - * - * If given a datetime string it will return a default value. - * - * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment - * - * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. - */ -export function parseTimeParts(start: string, end: string): QuickSelect | null { - const value = isNow(start) ? end : start; - - const matches = isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/); - - if (!matches) { - return null; - } - - const operator = matches[2]; - const matchedTimeValue = matches[3]; - const timeUnits = matches[4] as TimeUnitId; - - if (matchedTimeValue && timeUnits && operator) { - return { - timeTense: operator === '+' ? NEXT : LAST, - timeUnits, - timeValue: parseInt(matchedTimeValue, 10), - }; - } - - const duration = moment.duration(moment().diff(dateMath.parse(value))); - let unitOp = ''; - for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { - const as = duration.as(relativeUnitsFromLargestToSmallest[i]); - if (as < 0) { - unitOp = '+'; - } - if (Math.abs(as) > 1) { - return { - timeValue: Math.round(Math.abs(as)), - timeUnits: relativeUnitsFromLargestToSmallest[i], - timeTense: unitOp === '+' ? NEXT : LAST, - }; - } - } - - return null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx deleted file mode 100644 index 46adba1dbde5..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx +++ /dev/null @@ -1,132 +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, { Fragment } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; -import { useSeriesStorage } from '../hooks/use_series_storage'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { - const { setSeries } = useSeriesStorage(); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - // we don't want to display report definition filters in new series view - if (isConfigure) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId, series }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - if ((filters.length === 0 && definitionFilters.length === 0) || !indexPattern) { - return null; - } - - return ( - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - negate={false} - indexPattern={indexPattern} - /> - - )} - {(notValues ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - indexPattern={indexPattern} - /> - - )} - - ))} - - {definitionFilters.map(({ field, values }) => - values ? ( - - {}} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ) : null - )} - - {(series.filters ?? []).length > 0 && !isPreview && ( - - { - setSeries(seriesId, { ...series, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx deleted file mode 100644 index 85d65dcac6ac..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx +++ /dev/null @@ -1,120 +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 { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { EuiBasicTable, EuiSpacer, EuiText } from '@elastic/eui'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesInfo } from './columns/series_info'; -import { SeriesDatePicker } from '../components/series_date_picker'; -import { NO_BREAK_DOWN_LABEL } from './columns/breakdowns'; - -interface EditItem { - id: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesViewer() { - const { allSeries, reportType } = useSeriesStorage(); - - const columns = [ - { - name: '', - field: 'id', - width: '10%', - render: (seriesId: number, { seriesConfig, series }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.name', { - defaultMessage: 'Name', - }), - field: 'id', - width: '15%', - render: (seriesId: number, { series }: EditItem) => {series.name}, - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'id', - width: '35%', - render: (seriesId: number, { series, seriesConfig }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - field: 'seriesId', - width: '10%', - render: (seriesId: number, { seriesConfig: { labels }, series }: EditItem) => ( - {series.breakdown ? labels[series.breakdown] : NO_BREAK_DOWN_LABEL} - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', - }), - width: '30%', - field: 'id', - render: (seriesId: number, { series }: EditItem) => ( - - ), - }, - ]; - - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeries.forEach((series, seriesIndex) => { - if (indexPatterns[series.dataType] && !isEmpty(series.reportDefinitions)) { - items.push({ - series, - id: seriesIndex, - seriesConfig: getDefaultConfigs({ - reportType, - dataType: series.dataType, - indexPattern: indexPatterns[series.dataType], - }), - }); - } - }); - - if (items.length === 0 && allSeries.length > 0) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 4bba0c221f3c..fbda2f4ff62e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; +import { ExistsFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: Array; + baseFilters?: PersistableFilter[] | ExistsFilter[]; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,7 +69,6 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { - name: string; time: { to: string; from: string; @@ -77,12 +76,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; + reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - hidden?: boolean; - color?: string; + isNew?: boolean; } export interface UrlFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx deleted file mode 100644 index e0b46102caba..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx +++ /dev/null @@ -1,85 +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, { RefObject, useEffect, useState } from 'react'; - -import { EuiTabs, EuiTab, EuiButtonIcon } from '@elastic/eui'; -import { useHistory, useParams } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesViewer } from '../series_viewer/series_viewer'; -import { PanelId } from '../exploratory_view'; - -const tabs = [ - { - id: 'preview' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', - }), - }, - { - id: 'configure' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.configureSeries', { - defaultMessage: 'Configure series', - }), - }, -]; - -type ViewTab = 'preview' | 'configure'; - -export function SeriesViews({ - seriesBuilderRef, - onSeriesPanelCollapse, -}: { - seriesBuilderRef: RefObject; - onSeriesPanelCollapse: (panel: PanelId) => void; -}) { - const params = useParams<{ mode: ViewTab }>(); - - const history = useHistory(); - - const [selectedTabId, setSelectedTabId] = useState('configure'); - - const onSelectedTabChanged = (id: ViewTab) => { - setSelectedTabId(id); - history.push('/exploratory-view/' + id); - }; - - useEffect(() => { - setSelectedTabId(params.mode); - }, [params.mode]); - - const renderTabs = () => { - return tabs.map((tab, index) => ( - onSelectedTabChanged(tab.id)} - isSelected={tab.id === selectedTabId} - key={index} - > - {tab.id === 'preview' && selectedTabId === 'preview' ? ( - - onSeriesPanelCollapse('seriesPanel')} - /> -  {tab.name} - - ) : ( - tab.name - )} - - )); - }; - - return ( -
- {renderTabs()} - {selectedTabId === 'preview' && } - {selectedTabId === 'configure' && } -
- ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx deleted file mode 100644 index db1f23ad9b6e..000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; -import { - allSeriesKey, - convertAllShortSeries, - NEW_SERIES_KEY, - useSeriesStorage, -} from '../hooks/use_series_storage'; -import { SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { BuilderItem, getSeriesToEdit } from '../series_editor/series_editor'; -import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; - -export function ViewActions() { - const [editorItems, setEditorItems] = useState([]); - const { - getSeries, - allSeries, - setSeries, - storage, - reportType, - autoApply, - setAutoApply, - applyChanges, - } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - const addSeries = () => { - const prevSeries = allSeries?.[0]; - const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; - const nextSeries = { name } as SeriesUrl; - - const nextSeriesId = allSeries.length; - - if (reportType === 'data-distribution') { - setSeries(nextSeriesId, { - ...nextSeries, - time: prevSeries?.time || DEFAULT_TIME, - } as SeriesUrl); - } else { - setSeries( - nextSeriesId, - prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) - ); - } - }; - - const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); - - const isAddDisabled = - !reportType || - ((reportType === ReportTypes.CORE_WEB_VITAL || - reportType === ReportTypes.DEVICE_DISTRIBUTION) && - allSeries.length > 0); - - return ( - - - setAutoApply(!autoApply)} - compressed - /> - - {!autoApply && ( - - applyChanges()} isDisabled={autoApply || noChanges} fill> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - )} - - - addSeries()} isDisabled={isAddDisabled}> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add series', - })} - - - - - ); -} - -const AUTO_APPLY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { - defaultMessage: 'Auto apply', -}); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888a..fc562fa80e26 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,24 +6,15 @@ */ import React, { useEffect, useState } from 'react'; -import { union, isEmpty } from 'lodash'; -import { - EuiComboBox, - EuiFormControlLayout, - EuiComboBoxOptionOption, - EuiFormRow, -} from '@elastic/eui'; +import { union } from 'lodash'; +import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set( - allowAllValuesSelection && (values ?? []).length > 0 - ? ['ALL_VALUES', ...(values ?? [])] - : values - ) + new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) ); return (uniqueValues ?? []).map((label) => ({ @@ -39,9 +30,7 @@ export function FieldValueCombobox({ loading, values, setQuery, - usePrependLabel = true, compressed = true, - required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -65,35 +54,29 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - const comboBox = ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - isInvalid={required && isEmpty(selectedValue)} - /> - ); - - return usePrependLabel ? ( + return ( - {comboBox} + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + /> - ) : ( - - {comboBox} - ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index cee3ab8aea28..f713af976822 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,7 +70,6 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, - allowExclusions = true, compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -174,8 +173,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} - allowExclusions={allowExclusions} isLoading={loading && !query && options.length === 0} + allowExclusions={true} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 6671c43dd8c7..556a8e705234 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -95,7 +95,6 @@ describe('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} - allowExclusions={true} /> ); @@ -120,7 +119,6 @@ describe('FieldValueSuggestions', () => { excludedValue={['Pak']} filters={[]} asCombobox={false} - allowExclusions={true} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 65e1d0932e4e..54114c760464 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,10 +28,7 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, - usePrependLabel, allowAllValuesSelection, - required, - allowExclusions = true, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -67,10 +64,7 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} - usePrependLabel={usePrependLabel} - allowExclusions={allowExclusions} allowAllValuesSelection={allowAllValuesSelection} - required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 73b3d78ce870..d857b39b074a 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,10 +23,7 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; - usePrependLabel?: boolean; - allowExclusions?: boolean; allowAllValuesSelection?: boolean; - required?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 9e7b96b02206..01d727071770 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,25 +18,21 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string | string[]; + value: string; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = - value instanceof Array && value.length > 1 - ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) - : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); - filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; - - filter.meta.value = value as string; + filter.meta.value = value; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; + filter.meta.type = 'phrase'; return filter; } @@ -44,10 +40,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string | string[]; + value: string; negate: boolean; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; - invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; + invertFilter: (val: { field: string; value: string; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index afc053604fcd..9d557a40b798 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,7 +6,6 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -27,7 +26,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - }> + ); diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 896aca79114d..61feba83431f 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -78,6 +78,10 @@ export function ObservabilityPageTemplate({ href, isSelected, onClick: (event) => { + if (entry.onClick) { + entry.onClick(event); + } + if ( event.button !== 0 || event.defaultPrevented || diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 198b4092b0ed..82a0fc39b851 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 53b5300e556c..3d63f7bdaeaf 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -20,15 +20,25 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_UUID as ALERT_UUID_TYPED, + ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, - ALERT_UUID, - RULE_CATEGORY, - RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_UUID as ALERT_UUID_NON_TYPED, + ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; import type { TopAlert, TopAlertResponse } from '../'; @@ -46,6 +56,14 @@ type AlertsFlyoutProps = { selectedAlertId?: string; } & EuiFlyoutProps; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; +const ALERT_RULE_CATEGORY: typeof ALERT_RULE_CATEGORY_TYPED = ALERT_RULE_CATEGORY_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function AlertsFlyout({ alert, alerts, @@ -113,7 +131,7 @@ export function AlertsFlyout({ title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { defaultMessage: 'Rule type', }), - description: alertData.fields[RULE_CATEGORY] ?? '-', + description: alertData.fields[ALERT_RULE_CATEGORY] ?? '-', }, ]; @@ -121,7 +139,7 @@ export function AlertsFlyout({ -

{alertData.fields[RULE_NAME]}

+

{alertData.fields[ALERT_RULE_NAME]}

{alertData.reason} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx index 28d211766cfe..395c2a5253ec 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx @@ -14,11 +14,7 @@ import { EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_UUID, -} from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_DURATION, ALERT_SEVERITY_LEVEL, ALERT_UUID } from '@kbn/rule-data-utils'; import React, { Suspense, useMemo, useState } from 'react'; import { LazyAlertsFlyout } from '../..'; import { asDuration } from '../../../common/utils/formatters'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 4bc9c40e6e91..f82446db2ebe 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -5,19 +5,28 @@ * 2.0. */ -import { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { + AlertConsumers as AlertConsumersTyped, + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; +import { + AlertConsumers as AlertConsumersNonTyped, + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { EuiButtonIcon, EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { Suspense, useState } from 'react'; -import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_STATUS, - ALERT_START, - RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; import type { TimelinesUIStart } from '../../../../timelines/public'; import type { TopAlert } from './'; @@ -35,6 +44,13 @@ import { decorateResponse } from './decorate_response'; import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; +const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + interface AlertsTableTGridProps { indexName: string; rangeFrom: string; @@ -108,7 +124,7 @@ export const columns: Array< defaultMessage: 'Reason', }), linkField: '*', - id: RULE_NAME, + id: ALERT_RULE_NAME, }, ]; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx index 1f5372c8f2fe..3c31b8cfda87 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx @@ -14,7 +14,15 @@ import { EuiPopoverTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RULE_ID, RULE_NAME } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; +import { + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import React, { useState } from 'react'; import { format, parse } from 'url'; @@ -23,16 +31,19 @@ import type { ActionProps } from '../../../../timelines/common'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { usePluginContext } from '../../hooks/use_plugin_context'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function RowCellActionsRender({ data }: ActionProps) { const { core, observabilityRuleTypeRegistry } = usePluginContext(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { prepend } = core.http.basePath; const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const parsedFields = parseTechnicalFields(dataFieldEs); - const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[RULE_NAME]!, + reason: parsedFields[ALERT_RULE_NAME]!, ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts index e177bea6c6da..7eb6d785779b 100644 --- a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts +++ b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts @@ -5,27 +5,39 @@ * 2.0. */ +import type { + ALERT_START as ALERT_START_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - RULE_ID, - RULE_NAME, - ALERT_STATUS, - ALERT_START, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_START as ALERT_START_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { TopAlertResponse, TopAlert } from '.'; import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry'; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function decorateResponse( alerts: TopAlertResponse[], observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry ): TopAlert[] { return alerts.map((alert) => { const parsedFields = parseTechnicalFields(alert); - const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[RULE_NAME]!, + reason: parsedFields[ALERT_RULE_NAME]!, ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index 8bb15682dc61..112932d49311 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -11,49 +11,54 @@ import { ALERT_ID, ALERT_SEVERITY_LEVEL, ALERT_SEVERITY_VALUE, + ALERT_RULE_TYPE_ID, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, + ALERT_RULE_PRODUCER, } from '@kbn/rule-data-utils'; export const apmAlertResponseExample = [ { - 'rule.id': ['apm.error_rate'], + [ALERT_RULE_TYPE_ID]: ['apm.error_rate'], 'service.name': ['opbeans-java'], - 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [180057000], [ALERT_STATUS]: ['open'], [ALERT_SEVERITY_LEVEL]: ['warning'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], - 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], 'event.action': ['active'], '@timestamp': ['2021-04-12T13:53:49.550Z'], [ALERT_ID]: ['apm.error_rate_opbeans-java_production'], [ALERT_START]: ['2021-04-12T13:50:49.493Z'], - 'kibana.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], 'event.kind': ['state'], - 'rule.category': ['Error count threshold'], + [ALERT_RULE_CATEGORY]: ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, { - 'rule.id': ['apm.error_rate'], + [ALERT_RULE_TYPE_ID]: ['apm.error_rate'], 'service.name': ['opbeans-java'], - 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [2419005000], [ALERT_END]: ['2021-04-12T13:49:49.446Z'], [ALERT_STATUS]: ['closed'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['32b940e1-3809-4c12-8eee-f027cbb385e2'], - 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], 'event.action': ['close'], '@timestamp': ['2021-04-12T13:49:49.446Z'], [ALERT_ID]: ['apm.error_rate_opbeans-java_production'], [ALERT_START]: ['2021-04-12T13:09:30.441Z'], - 'kibana.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], 'event.kind': ['state'], - 'rule.category': ['Error count threshold'], + [ALERT_RULE_CATEGORY]: ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, @@ -158,7 +163,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'kibana.producer', + name: [ALERT_RULE_PRODUCER], type: 'string', esTypes: ['keyword'], searchable: true, @@ -174,7 +179,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.category', + name: [ALERT_RULE_CATEGORY], type: 'string', esTypes: ['keyword'], searchable: true, @@ -182,7 +187,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.id', + name: [ALERT_RULE_TYPE_ID], type: 'string', esTypes: ['keyword'], searchable: true, @@ -190,7 +195,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.name', + name: [ALERT_RULE_NAME], type: 'string', esTypes: ['keyword'], searchable: true, @@ -198,7 +203,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.uuid', + name: [ALERT_RULE_UUID], type: 'string', esTypes: ['keyword'], searchable: true, diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index cac3240cd200..dde10b7a3f3e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -7,13 +7,21 @@ import { EuiIconTip, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_STATUS, - ALERT_START, - RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; @@ -23,6 +31,12 @@ import { TopAlert } from '.'; import { decorateResponse } from './decorate_response'; import { usePluginContext } from '../../hooks/use_plugin_context'; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export const getMappedNonEcsValue = ({ data, fieldName, @@ -93,7 +107,7 @@ export const getRenderCellValue = ({ return asDuration(Number(value)); case ALERT_SEVERITY_LEVEL: return ; - case RULE_NAME: + case ALERT_RULE_NAME: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const decoratedAlerts = decorateResponse( [dataFieldEs] ?? [], diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 334733e36349..71b83b9e0532 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,7 +24,6 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; -import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -57,7 +56,6 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; - discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 09d22496c98f..f97e3fb99644 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { alertStatusRt } from '../../common/typings'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; @@ -100,20 +99,7 @@ export const routes = { }), }, }, - '/exploratory-view/': { - handler: () => { - return ; - }, - params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), - }), - }, - }, - '/exploratory-view/:mode': { + '/exploratory-view': { handler: () => { return ; }, @@ -126,4 +112,18 @@ export const routes = { }), }, }, + // enable this to test multi series architecture + // '/exploratory-view/multi': { + // handler: () => { + // return ; + // }, + // params: { + // query: t.partial({ + // rangeFrom: t.string, + // rangeTo: t.string, + // refreshPaused: jsonRt.pipe(t.boolean), + // refreshInterval: jsonRt.pipe(t.number), + // }), + // }, + // }, }; diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts index 79a36731f7ed..4789e4c5ea57 100644 --- a/x-pack/plugins/observability/public/services/navigation_registry.ts +++ b/x-pack/plugins/observability/public/services/navigation_registry.ts @@ -28,6 +28,8 @@ export interface NavigationEntry { matchFullPath?: boolean; // whether to ignore trailing slashes, defaults to `true` ignoreTrailingSlash?: boolean; + // handler to be called when the item is clicked + onClick?: (event: React.MouseEvent) => void; } export interface NavigationRegistry { diff --git a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts index db8191136686..74e6a5bc177f 100644 --- a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts +++ b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils/target/technical_field_names'; +import { EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils'; import { RuleDataClient } from '../../../../rule_registry/server'; import type { AlertStatus } from '../../../common/typings'; import { kqlQuery, rangeQuery, alertStatusQuery } from '../../utils/queries'; diff --git a/x-pack/plugins/observability/server/utils/queries.test.ts b/x-pack/plugins/observability/server/utils/queries.test.ts index a0a63b73d717..dab42fa604dc 100644 --- a/x-pack/plugins/observability/server/utils/queries.test.ts +++ b/x-pack/plugins/observability/server/utils/queries.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_STATUS } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; import * as queries from './queries'; describe('queries', () => { diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 2ee3291e7fb6..283779c31290 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { ALERT_STATUS } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; import { esKuery } from '../../../../../src/plugins/data/server'; import { AlertStatus } from '../../common/typings'; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 8aa184bca913..4e912ee4535b 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts index 7ef7469a5ebe..3735778b8749 100644 --- a/x-pack/plugins/osquery/common/typed_json.ts +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -6,7 +6,7 @@ */ import { DslQuery, Filter } from '@kbn/es-query'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export type ESQuery = | ESRangeQuery diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts index d195198e54a7..7244066f798b 100644 --- a/x-pack/plugins/osquery/common/types.ts +++ b/x-pack/plugins/osquery/common/types.ts @@ -9,8 +9,11 @@ import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../ export const savedQuerySavedObjectType = 'osquery-saved-query'; export const packSavedObjectType = 'osquery-pack'; -export const usageMetricSavedObjectType = 'osquery-usage-metric'; -export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack' | 'osquery-usage-metric'; +export const usageMetricSavedObjectType = 'osquery-manager-usage-metric'; +export type SavedObjectType = + | 'osquery-saved-query' + | 'osquery-pack' + | 'osquery-manager-usage-metric'; /** * This makes any optional property the same as Required would but also has the diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json index 467ea13fc486..1adb067fe682 100644 --- a/x-pack/plugins/osquery/cypress/tsconfig.json +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -1,11 +1,13 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [], "include": [ - "./**/*" + "**/*" + ], + "exclude": [ + "target/**/*" ], "compilerOptions": { - "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress", + "outDir": "target/types", "types": [ "cypress", "node" diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 75277059bbf9..083d0193be2a 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -15,6 +15,7 @@ import { AgentIdToName } from '../agents/agent_id_to_name'; import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; +import { useActionResultsPrivileges } from './use_action_privileges'; interface ActionResultsSummaryProps { actionId: string; @@ -41,6 +42,7 @@ const ActionResultsSummaryComponent: React.FC = ({ expirationDate, ]); const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -52,6 +54,7 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); if (expired) { // @ts-expect-error update types @@ -77,6 +80,7 @@ const ActionResultsSummaryComponent: React.FC = ({ }, ], isLive, + skip: !hasActionResultsPrivileges, }); const renderAgentIdColumn = useCallback((agentId) => , []); diff --git a/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx new file mode 100644 index 000000000000..2c80c874e89f --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx @@ -0,0 +1,33 @@ +/* + * 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 { useQuery } from 'react-query'; + +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../common/lib/kibana'; +import { useErrorToast } from '../common/hooks/use_error_toast'; + +export const useActionResultsPrivileges = () => { + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); + + return useQuery( + ['actionResultsPrivileges'], + () => http.get('/internal/osquery/privileges_check'), + { + keepPreviousData: true, + select: (response) => response?.has_all_requested ?? false, + onSuccess: () => setErrorToast(), + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.action_results_privileges.fetchError', { + defaultMessage: 'Error while fetching action results privileges', + }), + }), + } + ); +}; diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index c51f2d2f44a5..74061915d3b8 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -9,11 +9,7 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { - agentPolicyRouteService, - GetAgentPoliciesResponse, - GetAgentPoliciesResponseItem, -} from '../../../fleet/common'; +import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = () => { @@ -22,12 +18,7 @@ export const useAgentPolicies = () => { return useQuery( ['agentPolicies'], - () => - http.get(agentPolicyRouteService.getListPath(), { - query: { - perPage: 100, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/agent_policies/'), { initialData: { items: [], total: 0, page: 1, perPage: 100 }, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index dcebf136b677..302567ef2564 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -9,7 +9,6 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; interface UseAgentPolicy { @@ -23,7 +22,7 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => { return useQuery( ['agentPolicy', { policyId }], - () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), { enabled: !skip, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 53e2ce1d5342..8a40cb171070 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -65,9 +65,13 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh osqueryPolicyData ); const grouper = useMemo(() => new AgentGrouper(), []); - const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, { - perPage, - }); + const { isLoading: agentsLoading, data: agents } = useAllAgents( + osqueryPolicyData, + debouncedSearchValue, + { + perPage, + } + ); // option related const [options, setOptions] = useState([]); @@ -108,8 +112,8 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh grouper.setTotalAgents(totalNumAgents); grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms); grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents!); + // @ts-expect-error update types + grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents); const newOptions = grouper.generateOptions(); setOptions(newOptions); }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_details.ts b/x-pack/plugins/osquery/public/agents/use_agent_details.ts index 1a0663812dec..b0c2fb2e1cba 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_details.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_details.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetOneAgentResponse, agentRouteService } from '../../../fleet/common'; +import { GetOneAgentResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -21,7 +21,7 @@ export const useAgentDetails = ({ agentId }: UseAgentDetails) => { const setErrorToast = useErrorToast(); return useQuery( ['agentDetails', agentId], - () => http.get(agentRouteService.getInfoPath(agentId)), + () => http.get(`/internal/osquery/fleet_wrapper/agents/${agentId}`), { enabled: agentId.length > 0, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts index 115b5af9d3a1..e8d6fe7eb97a 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts @@ -9,7 +9,7 @@ import { mapKeys } from 'lodash'; import { useQueries, UseQueryResult } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common'; +import { GetOneAgentPolicyResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = (policyIds: string[] = []) => { @@ -19,7 +19,7 @@ export const useAgentPolicies = (policyIds: string[] = []) => { const agentResponse = useQueries( policyIds.map((policyId) => ({ queryKey: ['agentPolicy', policyId], - queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + queryFn: () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), enabled: policyIds.length > 0, onSuccess: () => setErrorToast(), onError: (error) => diff --git a/x-pack/plugins/osquery/public/agents/use_agent_status.ts b/x-pack/plugins/osquery/public/agents/use_agent_status.ts index c8bc8d2fe5c0..ba2237dbe57e 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_status.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_status.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentStatusResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -25,7 +25,7 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => { ['agentStatus', policyId], () => http.get( - agentRouteService.getStatusPath(), + `/internal/osquery/fleet_wrapper/agent-status`, policyId ? { query: { diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index fac43eaa7ffc..42e4954989c6 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentsResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentsResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -31,7 +31,8 @@ export const useAllAgents = ( const { perPage } = opts; const { http } = useKibana().services; const setErrorToast = useErrorToast(); - const { isLoading: agentsLoading, data: agentData } = useQuery( + + return useQuery( ['agents', osqueryPolicies, searchValue, perPage], () => { let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`; @@ -40,7 +41,7 @@ export const useAllAgents = ( kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } - return http.get(agentRouteService.getListPath(), { + return http.get(`/internal/osquery/fleet_wrapper/agents`, { query: { kuery, perPage, @@ -48,6 +49,8 @@ export const useAllAgents = ( }); }, { + // @ts-expect-error update types + select: (data) => data?.agents || [], enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0, onSuccess: () => setErrorToast(), onError: (error) => @@ -58,6 +61,4 @@ export const useAllAgents = ( }), } ); - - return { agentsLoading, agents: agentData?.list }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 9064dac1ae5d..4b9ff931f3a9 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -10,8 +10,6 @@ import { useQuery } from 'react-query'; import { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useOsqueryPolicies = () => { @@ -20,12 +18,7 @@ export const useOsqueryPolicies = () => { const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery( ['osqueryPolicies'], - () => - http.get(packagePolicyRouteService.getListPath(), { - query: { - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/package_policies'), { select: (response) => uniq(response.items.map((p: { policy_id: string }) => p.policy_id)), diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index 236fdb1af181..58f9f8dbec61 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -6,11 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { find } from 'lodash/fp'; import { useQuery } from 'react-query'; -import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; import { useKibana } from '../lib/kibana'; import { useErrorToast } from './use_error_toast'; @@ -18,23 +15,12 @@ export const useOsqueryIntegration = () => { const { http } = useKibana().services; const setErrorToast = useErrorToast(); - return useQuery( - 'integrations', - () => - http.get(epmRouteService.getListPath(), { - query: { - experimental: true, - }, - }), - { - select: ({ response }: GetPackagesResponse) => - find(['name', OSQUERY_INTEGRATION_NAME], response), - onError: (error: Error) => - setErrorToast(error, { - title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { - defaultMessage: 'Error while fetching osquery integration', - }), + return useQuery('integration', () => http.get('/internal/osquery/status'), { + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { + defaultMessage: 'Error while fetching osquery integration', }), - } - ); + }), + }); }; diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx index 5be2b1816ad8..8c844d9eda3b 100644 --- a/x-pack/plugins/osquery/public/editor/index.tsx +++ b/x-pack/plugins/osquery/public/editor/index.tsx @@ -28,13 +28,13 @@ interface OsqueryEditorProps { const OsqueryEditorComponent: React.FC = ({ defaultValue, - // disabled, + disabled, onChange, }) => ( ( + <> + + + + + + + +); + +export const DisabledCallout = React.memo(DisabledCalloutComponent); diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx index 775b5c7a06d2..67791cb34e68 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx @@ -5,16 +5,42 @@ * 2.0. */ -import React from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import React, { useEffect } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../fleet/public'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; +import { useKibana } from '../common/lib/kibana'; /** * Exports Osquery-specific package policy instructions * for use in the Fleet app custom tab */ export const OsqueryManagedCustomButtonExtension = React.memo( - () => + () => { + const [disabled, setDisabled] = React.useState(null); + const { http } = useKibana().services; + + useEffect(() => { + const fetchStatus = () => { + http.get('/internal/osquery/status').then((response) => { + setDisabled(response.install_status !== 'installed'); + }); + }; + fetchStatus(); + }, [http]); + + if (disabled === null) { + return ; + } + + return ( + <> + {disabled ? : null} + + + ); + } ); OsqueryManagedCustomButtonExtension.displayName = 'OsqueryManagedCustomButtonExtension'; diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 63036f5f693f..9fd3c9b032ef 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -11,7 +11,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { produce } from 'immer'; -import { i18n } from '@kbn/i18n'; import { agentRouteService, agentPolicyRouteService, @@ -29,6 +28,7 @@ import { import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table'; import { useKibana } from '../common/lib/kibana'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; import { OsqueryManagerPackagePolicy } from '../../common/types'; /** @@ -163,22 +163,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< return ( <> - {!editMode ? ( - <> - - - - - - - - ) : null} + {!editMode ? : null} {policyAgentsCount === 0 ? ( <> diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 8654a74fecfb..bf614ff4e9bc 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -47,6 +47,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, onSuccess, }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { http } = useKibana().services; const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const setErrorToast = useErrorToast(); @@ -175,7 +176,12 @@ const LiveQueryFormComponent: React.FC = ({ {!agentId && ( = ({ [ agentId, agentSelected, + permissions.writeSavedQueries, handleShowSaveQueryFlout, queryComponentProps, queryValueProvided, diff --git a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx index 070339bb58af..c79fae9eb5d2 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiCodeBlock, EuiFormRow, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useRef } from 'react'; +import styled from 'styled-components'; import { OsquerySchemaLink } from '../../components/osquery_schema_link'; import { FieldHook } from '../../shared_imports'; @@ -15,6 +16,11 @@ import { SavedQueriesDropdown, SavedQueriesDropdownRef, } from '../../saved_queries/saved_queries_dropdown'; +import { useKibana } from '../../common/lib/kibana'; + +const StyledEuiCodeBlock = styled(EuiCodeBlock)` + min-height: 150px; +`; interface LiveQueryQueryFieldProps { disabled?: boolean; @@ -22,6 +28,7 @@ interface LiveQueryQueryFieldProps { } const LiveQueryQueryFieldComponent: React.FC = ({ disabled, field }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { value, setValue, errors } = field; const error = errors[0]?.message; const savedQueriesDropdownRef = useRef(null); @@ -46,12 +53,23 @@ const LiveQueryQueryFieldComponent: React.FC = ({ disa <> }> - + {!permissions.writeLiveQueries ? ( + + {value} + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 12f9025e406d..8555997d6178 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject, Subject } from 'rxjs'; import { AppMountParameters, CoreSetup, @@ -13,9 +12,6 @@ import { PluginInitializerContext, CoreStart, DEFAULT_APP_CATEGORIES, - AppStatus, - AppNavLinkStatus, - AppUpdater, } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { @@ -25,7 +21,6 @@ import { AppPluginStartDependencies, } from './types'; import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common'; -import { Installation } from '../../fleet/common'; import { LazyOsqueryManagedPolicyCreateImportExtension, LazyOsqueryManagedPolicyEditExtension, @@ -33,48 +28,7 @@ import { } from './fleet_integration'; import { getLazyOsqueryAction } from './shared_components'; -export function toggleOsqueryPlugin( - updater$: Subject, - http: CoreStart['http'], - registerExtension?: StartPlugins['fleet']['registerExtension'] -) { - if (http.anonymousPaths.isAnonymous(window.location.pathname)) { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - return; - } - - http - .fetch(`/internal/osquery/status`) - .then((response) => { - const installed = response?.install_status === 'installed'; - - if (installed && registerExtension) { - registerExtension({ - package: OSQUERY_INTEGRATION_NAME, - view: 'package-detail-custom', - Component: LazyOsqueryManagedCustomButtonExtension, - }); - } - - updater$.next(() => ({ - navLinkStatus: installed ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, - })); - }) - .catch(() => { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - }); -} - export class OsqueryPlugin implements Plugin { - private readonly appUpdater$ = new BehaviorSubject(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, - })); private kibanaVersion: string; private storage = new Storage(localStorage); @@ -102,8 +56,6 @@ export class OsqueryPlugin implements Plugin ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); + + registerExtension({ + package: OSQUERY_INTEGRATION_NAME, + view: 'package-detail-custom', + Component: LazyOsqueryManagedCustomButtonExtension, + }); } return { diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index d82737ab51e7..d293847215d6 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -8,6 +8,7 @@ import { isEmpty, isEqual, keys, map } from 'lodash/fp'; import { EuiCallOut, + EuiCode, EuiDataGrid, EuiDataGridSorting, EuiDataGridProps, @@ -31,6 +32,8 @@ import { ViewResultsInLensAction, ViewResultsActionButtonType, } from '../scheduled_query_groups/scheduled_query_group_queries_table'; +import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; +import { OSQUERY_INTEGRATION_NAME } from '../../common'; const DataContext = createContext([]); @@ -49,6 +52,7 @@ const ResultsTableComponent: React.FC = ({ endDate, }) => { const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations }, @@ -60,6 +64,7 @@ const ResultsTableComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); const { getUrlForApp } = useKibana().services.application; @@ -104,6 +109,7 @@ const ResultsTableComponent: React.FC = ({ field: sortedColumn.id, direction: sortedColumn.direction as Direction, })), + skip: !hasActionResultsPrivileges, }); const [visibleColumns, setVisibleColumns] = useState([]); @@ -237,6 +243,17 @@ const ResultsTableComponent: React.FC = ({ ] ); + if (!hasActionResultsPrivileges) { + return ( + +

+ You're missing read privileges to read from + logs-{OSQUERY_INTEGRATION_NAME}.result*. +

+
+ ); + } + if (!isFetched) { return ; } diff --git a/x-pack/plugins/osquery/public/routes/components/index.ts b/x-pack/plugins/osquery/public/routes/components/index.ts new file mode 100644 index 000000000000..877c25fe7cda --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './missing_privileges'; diff --git a/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx b/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx new file mode 100644 index 000000000000..6adabff59912 --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/missing_privileges.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 React from 'react'; +import { EuiEmptyPrompt, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; + +const Panel = styled(EuiPanel)` + max-width: 500px; + margin-right: auto; + margin-left: auto; +`; + +const MissingPrivilegesComponent = () => ( +
+ + + + +

+ } + body={ +

+ +

+ } + /> + + +
+); + +export const MissingPrivileges = React.memo(MissingPrivilegesComponent); diff --git a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx index af039e85e978..47815516dd72 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx @@ -12,15 +12,26 @@ import { LiveQueriesPage } from './list'; import { NewLiveQueryPage } from './new'; import { LiveQueryDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const LiveQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const match = useRouteMatch(); + if (!permissions.readLiveQueries) { + return ; + } + return ( - + {permissions.runSavedQueries || permissions.writeLiveQueries ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx index 90ac7b5cc17a..23bc44b45540 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx @@ -9,13 +9,14 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { ActionsTable } from '../../../actions/actions_table'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const LiveQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const newQueryLinkProps = useRouterNavigate('live_queries/new'); @@ -40,14 +41,19 @@ const LiveQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeLiveQueries, permissions.runSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx index 8d77b7819bd3..a7596575b90c 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx @@ -24,11 +24,13 @@ import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_f interface EditSavedQueryFormProps { defaultValue?: unknown; handleSubmit: () => Promise; + viewMode?: boolean; } const EditSavedQueryFormComponent: React.FC = ({ defaultValue, handleSubmit, + viewMode, }) => { const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -39,41 +41,45 @@ const EditSavedQueryFormComponent: React.FC = ({ return (
- - - - - + + {!viewMode && ( + <> + + - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + )} ); }; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 5bdba133fad7..401966460a7e 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -17,7 +17,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { useParams } from 'react-router-dom'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; @@ -25,6 +25,8 @@ import { EditSavedQueryForm } from './form'; import { useDeleteSavedQuery, useUpdateSavedQuery, useSavedQuery } from '../../../saved_queries'; const EditSavedQueryPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const { savedQueryId } = useParams<{ savedQueryId: string }>(); const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -35,6 +37,8 @@ const EditSavedQueryPageComponent = () => { useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); + const viewMode = useMemo(() => !permissions.writeSavedQueries, [permissions.writeSavedQueries]); + const handleCloseDeleteConfirmationModal = useCallback(() => { setIsDeleteModalVisible(false); }, []); @@ -63,21 +67,32 @@ const EditSavedQueryPageComponent = () => {

- + {viewMode ? ( + + ) : ( + + )}

), - [savedQueryDetails?.attributes?.id, savedQueryListProps] + [savedQueryDetails?.attributes?.id, savedQueryListProps, viewMode] ); const RightColumn = useMemo( @@ -95,12 +110,17 @@ const EditSavedQueryPageComponent = () => { if (isLoading) return null; return ( - + {!isLoading && !isEmpty(savedQueryDetails) && ( )} {isDeleteModalVisible ? ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx index f986129bdfef..a2241ae017df 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx @@ -12,15 +12,22 @@ import { QueriesPage } from './list'; import { NewSavedQueryPage } from './new'; import { EditSavedQueryPage } from './edit'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { MissingPrivileges } from '../components'; +import { useKibana } from '../../common/lib/kibana'; const SavedQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('saved_queries'); const match = useRouteMatch(); + if (!permissions.readSavedQueries) { + return ; + } + return ( - + {permissions.writeSavedQueries ? : } diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index 0c04e816dae7..e82dcf85780e 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -21,16 +21,63 @@ import { useHistory } from 'react-router-dom'; import { SavedObject } from 'kibana/public'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; import { useSavedQueries } from '../../../saved_queries/use_saved_queries'; +interface PlayButtonProps { + disabled: boolean; + savedQueryId: string; + savedQueryName: string; +} + +const PlayButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { + const { push } = useHistory(); + + // TODO: Fix href + const handlePlayClick = useCallback( + () => + push('/live_queries/new', { + form: { + savedQueryId, + }, + }), + [push, savedQueryId] + ); + + return ( + + ); +}; + +const PlayButton = React.memo(PlayButtonComponent); + interface EditButtonProps { + disabled?: boolean; savedQueryId: string; savedQueryName: string; } -const EditButtonComponent: React.FC = ({ savedQueryId, savedQueryName }) => { +const EditButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`); return ( @@ -38,6 +85,7 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue color="primary" {...buttonProps} iconType="pencil" + isDisabled={disabled} aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', { defaultMessage: 'Edit {savedQueryName}', values: { @@ -51,8 +99,9 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue const EditButton = React.memo(EditButtonComponent); const SavedQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + useBreadcrumbs('saved_queries'); - const { push } = useHistory(); const newQueryLinkProps = useRouterNavigate('saved_queries/new'); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(20); @@ -61,16 +110,6 @@ const SavedQueriesPageComponent = () => { const { data } = useSavedQueries({ isLive: true }); - const handlePlayClick = useCallback( - (item) => - push('/live_queries/new', { - form: { - savedQueryId: item.id, - }, - }), - [push] - ); - const renderEditAction = useCallback( (item: SavedObject<{ name: string }>) => ( @@ -78,6 +117,17 @@ const SavedQueriesPageComponent = () => { [] ); + const renderPlayAction = useCallback( + (item: SavedObject<{ name: string }>) => ( + + ), + [permissions.runSavedQueries, permissions.writeLiveQueries] + ); + const renderUpdatedAt = useCallback((updatedAt, item) => { if (!updatedAt) return '-'; @@ -128,17 +178,10 @@ const SavedQueriesPageComponent = () => { name: i18n.translate('xpack.osquery.savedQueries.table.actionsColumnTitle', { defaultMessage: 'Actions', }), - actions: [ - { - type: 'icon', - icon: 'play', - onClick: handlePlayClick, - }, - { render: renderEditAction }, - ], + actions: [{ render: renderPlayAction }, { render: renderEditAction }], }, ], - [handlePlayClick, renderEditAction, renderUpdatedAt] + [renderEditAction, renderPlayAction, renderUpdatedAt] ); const onTableChange = useCallback(({ page = {}, sort = {} }) => { @@ -189,14 +232,19 @@ const SavedQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx index 960de043eac6..dc6df4961509 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx @@ -21,7 +21,7 @@ import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group'; import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table'; @@ -36,6 +36,7 @@ const Divider = styled.div` `; const ScheduledQueryGroupDetailsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>(); const scheduledQueryGroupsListProps = useRouterNavigate('scheduled_query_groups'); const editQueryLinkProps = useRouterNavigate( @@ -111,7 +112,12 @@ const ScheduledQueryGroupDetailsPageComponent = () => { - + { ), - [data?.policy_id, editQueryLinkProps] + [data?.policy_id, editQueryLinkProps, permissions] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx index 76ca2bf14d30..53bf4ae79a90 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx @@ -13,18 +13,25 @@ import { AddScheduledQueryGroupPage } from './add'; import { EditScheduledQueryGroupPage } from './edit'; import { ScheduledQueryGroupDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const ScheduledQueryGroupsComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('scheduled_query_groups'); const match = useRouteMatch(); + if (!permissions.readPacks) { + return ; + } + return ( - + {permissions.writePacks ? : } - + {permissions.writePacks ? : } diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx index b02ef95498b5..006dd0e6ec1b 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx @@ -9,12 +9,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { ScheduledQueryGroupsTable } from '../../../scheduled_query_groups/scheduled_query_groups_table'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const ScheduledQueryGroupsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const newQueryLinkProps = useRouterNavigate('scheduled_query_groups/add'); const LeftColumn = useMemo( @@ -38,14 +39,19 @@ const ScheduledQueryGroupsPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [newQueryLinkProps, permissions.writePacks] ); return ( diff --git a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx index 9bbf847c4d2a..beff34a8919a 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,64 +17,78 @@ import { CodeEditorField } from './code_editor_field'; export const CommonUseField = getUseField({ component: Field }); -const SavedQueryFormComponent = () => ( - <> - - - - - - - - - -
+interface SavedQueryFormProps { + viewMode?: boolean; +} + +const SavedQueryFormComponent: React.FC = ({ viewMode }) => { + const euiFieldProps = useMemo( + () => ({ + isDisabled: !!viewMode, + }), + [viewMode] + ); + + return ( + <> + + + + + + + + + +
+ +
+
+ -
-
- - +
+
+ + + + + + - - - - - - - - - - - - - - - - -); + + + + + + + + ); +}; export const SavedQueryForm = React.memo(SavedQueryFormComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx index bcb47d0adc83..7f26534626b1 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx @@ -28,12 +28,16 @@ const StyledEuiLoadingSpinner = styled(EuiLoadingSpinner)` `; interface ActiveStateSwitchProps { + disabled?: boolean; item: PackagePolicy; } const ActiveStateSwitchComponent: React.FC = ({ item }) => { const queryClient = useQueryClient(); const { + application: { + capabilities: { osquery: permissions }, + }, http, notifications: { toasts }, } = useKibana().services; @@ -126,7 +130,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) {isLoading && } ( ['scheduledQueryGroup', { scheduledQueryGroupId }], - () => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)), + () => http.get(`/internal/osquery/scheduled_query_group/${scheduledQueryGroupId}`), { keepPreviousData: true, enabled: !skip || !scheduledQueryGroupId, diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts index 3302d8e621eb..01b67a3d5164 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts @@ -9,12 +9,7 @@ import { produce } from 'immer'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { - ListResult, - PackagePolicy, - packagePolicyRouteService, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '../../../fleet/common'; +import { ListResult, PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; export const useScheduledQueryGroups = () => { @@ -23,7 +18,7 @@ export const useScheduledQueryGroups = () => { return useQuery>( ['scheduledQueries'], () => - http.get(packagePolicyRouteService.getListPath(), { + http.get('/internal/osquery/scheduled_query_group', { query: { page: 1, perPage: 10000, diff --git a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts index 6ebf469b8fb2..ca4fd1ebeffd 100644 --- a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts +++ b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, LoggerFactory } from 'src/core/server'; +import { CoreSetup, Logger, LoggerFactory } from '../../../../../src/core/server'; import { SecurityPluginStart } from '../../../security/server'; import { AgentService, @@ -71,6 +71,7 @@ export interface OsqueryAppContext { logFactory: LoggerFactory; config(): ConfigType; security: SecurityPluginStart; + getStartServices: CoreSetup['getStartServices']; /** * Object readiness is tied to plugin start method */ diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 6bc12f5736e5..ff8483fdb385 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,14 +5,20 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, +} from '../../fleet/common'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, + DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; - import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; @@ -21,6 +27,169 @@ import { initSavedObjects } from './saved_objects'; import { initUsageCollectors } from './usage'; import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_context_services'; import { ConfigType } from './config'; +import { packSavedObjectType, savedQuerySavedObjectType } from '../common/types'; +import { PLUGIN_ID } from '../common'; + +const registerFeatures = (features: SetupPlugins['features']) => { + features.registerKibanaFeature({ + id: PLUGIN_ID, + name: i18n.translate('xpack.osquery.features.osqueryFeatureName', { + defaultMessage: 'Osquery', + }), + category: DEFAULT_APP_CATEGORIES.management, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + order: 2300, + excludeFromBasePrivileges: true, + privileges: { + all: { + api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-write`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + read: [PACKAGES_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE], + }, + ui: ['write'], + }, + read: { + api: [`${PLUGIN_ID}-read`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [], + read: [ + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + ], + }, + ui: ['read'], + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.osquery.features.liveQueriesSubFeatureName', { + defaultMessage: 'Live queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writeLiveQueries`, `${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeLiveQueries', 'readLiveQueries'], + }, + { + api: [`${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readLiveQueries'], + }, + ], + }, + { + groupType: 'independent', + privileges: [ + { + api: [`${PLUGIN_ID}-runSavedQueries`], + id: 'run_saved_queries', + name: i18n.translate('xpack.osquery.features.runSavedQueriesPrivilegeName', { + defaultMessage: 'Run Saved queries', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: ['runSavedQueries'], + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.osquery.features.savedQueriesSubFeatureName', { + defaultMessage: 'Saved queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'saved_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [savedQuerySavedObjectType], + read: [], + }, + ui: ['writeSavedQueries', 'readSavedQueries'], + }, + { + id: 'saved_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [savedQuerySavedObjectType], + }, + ui: ['readSavedQueries'], + }, + ], + }, + ], + }, + { + // TODO: Rename it to "Packs" as part of https://github.com/elastic/kibana/pull/107345 + name: i18n.translate('xpack.osquery.features.scheduledQueryGroupsSubFeatureName', { + defaultMessage: 'Scheduled query groups', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writePacks`], + id: 'packs_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [packSavedObjectType], + read: [], + }, + ui: ['writePacks', 'readPacks'], + }, + { + api: [`${PLUGIN_ID}-readPacks`], + id: 'packs_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [packSavedObjectType], + }, + ui: ['readPacks'], + }, + ], + }, + ], + }, + ], + }); +}; export class OsqueryPlugin implements Plugin { private readonly logger: Logger; @@ -40,10 +209,13 @@ export class OsqueryPlugin implements Plugin config, security: plugins.security, diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 478bfc1053bd..79c1149675b0 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import moment from 'moment'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -19,6 +20,7 @@ import { } from '../../../common/schemas/routes/action/create_action_request_body_schema'; import { incrementCount } from '../usage'; +import { getInternalSavedObjectsClient } from '../../usage/collector'; export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( @@ -30,10 +32,17 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon CreateActionRequestBodySchema >(createActionRequestBodySchema), }, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`, `access:${PLUGIN_ID}-runSavedQueries`], + }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + const esClient = context.core.elasticsearch.client.asInternalUser; const soClient = context.core.savedObjects.client; + const internalSavedObjectsClient = await getInternalSavedObjectsClient( + osqueryContext.getStartServices + ); + const { agentSelection } = request.body as { agentSelection: AgentSelection }; const selectedAgents = await parseAgentSelection( esClient, @@ -41,12 +50,14 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon osqueryContext, agentSelection ); - incrementCount(soClient, 'live_query'); + incrementCount(internalSavedObjectsClient, 'live_query'); if (!selectedAgents.length) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.badRequest({ body: new Error('No agents found for selection') }); } + // TODO: Add check for `runSavedQueries` only + try { const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username; const action = { @@ -74,7 +85,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon }, }); } catch (error) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.customError({ statusCode: 500, body: new Error(`Error occurred while processing ${error}`), diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts new file mode 100644 index 000000000000..67b4a27ab9ec --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts @@ -0,0 +1,33 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents/{id}', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agent = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.getAgent(esClient, request.params.id); + + return response.ok({ body: { item: agent } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts new file mode 100644 index 000000000000..e35e776cb195 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const agentPolicies = await osqueryContext.service.getAgentPolicyService()?.list(soClient, { + ...(request.query || {}), + perPage: 100, + }); + + return response.ok({ body: agentPolicies }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts new file mode 100644 index 000000000000..f845b04e99c9 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const packageInfo = await osqueryContext.service + .getAgentPolicyService() + ?.get(soClient, request.params.id); + + return response.ok({ body: { item: packageInfo } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts new file mode 100644 index 000000000000..dea440247295 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { GetAgentStatusResponse } from '../../../../fleet/common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentStatusForAgentPolicyRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent-status', + validate: { + query: schema.object({ + policyId: schema.string(), + kuery: schema.maybe(schema.string()), + }), + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const results = await osqueryContext.service + .getAgentService() + ?.getAgentStatusForAgentPolicy(esClient, request.query.policyId, request.query.kuery); + + if (!results) { + return response.ok({ body: {} }); + } + + const body: GetAgentStatusResponse = { results }; + + return response.ok({ body }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts new file mode 100644 index 000000000000..d45cb26e0d19 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts @@ -0,0 +1,33 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agents = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.listAgents(esClient, request.query); + + return response.ok({ body: agents }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts similarity index 81% rename from x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts index 43d5f3fc893f..b95dfbdfb9cb 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts @@ -6,19 +6,19 @@ */ import { schema } from '@kbn/config-schema'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; - +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const findScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { - path: '/internal/osquery/scheduled_query', + path: '/internal/osquery/fleet_wrapper/package_policies', validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts new file mode 100644 index 000000000000..1821e19da975 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import { getAgentPoliciesRoute } from './get_agent_policies'; +import { getAgentPolicyRoute } from './get_agent_policy'; +import { getAgentStatusForAgentPolicyRoute } from './get_agent_status_for_agent_policy'; +import { getPackagePoliciesRoute } from './get_package_policies'; +import { getAgentsRoute } from './get_agents'; +import { getAgentDetailsRoute } from './get_agent_details'; + +export const initFleetWrapperRoutes = (router: IRouter, context: OsqueryAppContext) => { + getAgentDetailsRoute(router, context); + getAgentPoliciesRoute(router, context); + getAgentPolicyRoute(router, context); + getAgentStatusForAgentPolicyRoute(router, context); + getPackagePoliciesRoute(router, context); + getAgentsRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index dd11141b2553..c927c711a23c 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -10,13 +10,19 @@ import { initActionRoutes } from './action'; import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initSavedQueryRoutes } from './saved_query'; import { initStatusRoutes } from './status'; +import { initFleetWrapperRoutes } from './fleet_wrapper'; import { initPackRoutes } from './pack'; +import { initScheduledQueryGroupRoutes } from './scheduled_query_group'; +import { initPrivilegesCheckRoutes } from './privileges_check'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { const config = context.config(); initActionRoutes(router, context); initStatusRoutes(router, context); + initScheduledQueryGroupRoutes(router, context); + initFleetWrapperRoutes(router, context); + initPrivilegesCheckRoutes(router, context); if (config.packs) { initPackRoutes(router); diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/index.ts b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts new file mode 100644 index 000000000000..8932b23b85f5 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { privilegesCheckRoute } from './privileges_check_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const initPrivilegesCheckRoutes = (router: IRouter, context: OsqueryAppContext) => { + privilegesCheckRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts new file mode 100644 index 000000000000..80c335c1c46d --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/privileges_check', + validate: {}, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`], + }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asCurrentUser; + + const privileges = ( + await esClient.security.hasPrivileges({ + body: { + index: [ + { + names: [`logs-${OSQUERY_INTEGRATION_NAME}.result*`], + privileges: ['read'], + }, + ], + }, + }) + ).body; + + return response.ok({ + body: privileges, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index a41cb7cc39b4..fe8220c559de 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { IRouter } from '../../../../../../src/core/server'; - +import { PLUGIN_ID } from '../../../common'; import { createSavedQueryRequestSchema, CreateSavedQueryRequestSchemaDecoded, @@ -24,6 +24,7 @@ export const createSavedQueryRoute = (router: IRouter) => { CreateSavedQueryRequestSchemaDecoded >(createSavedQueryRequestSchema), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts index 5b8e231ba61e..a34db8c11ddc 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const deleteSavedQueryRoute = (router: IRouter) => { validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts index 6d737ba0d022..79d6927d0672 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const findSavedQueryRoute = (router: IRouter) => { validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts index 2d399648df4c..4157ed158230 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const readSavedQueryRoute = (router: IRouter) => { validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index f9ecf675489d..8edf95e31154 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts index a3b882392989..831fb30f6e32 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts @@ -6,16 +6,18 @@ */ import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; export const createScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { - path: '/internal/osquery/scheduled', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asCurrentUser; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts index 5b8e231ba61e..c914512bb155 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts @@ -6,17 +6,18 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; export const deleteSavedQueryRoute = (router: IRouter) => { router.delete( { - path: '/internal/osquery/saved_query', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts new file mode 100644 index 000000000000..15c45e09b1bf --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.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 { schema } from '@kbn/config-schema'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const findScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/scheduled_query_group', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, + }, + async (context, request, response) => { + const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; + const packagePolicyService = osqueryContext.service.getPackagePolicyService(); + const policies = await packagePolicyService?.list(context.core.savedObjects.client, { + kuery, + }); + + return response.ok({ + body: policies, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts similarity index 67% rename from x-pack/plugins/osquery/server/routes/scheduled_query/index.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts index 706bc3839729..416981a5cb5f 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts @@ -10,14 +10,14 @@ import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; // import { createScheduledQueryRoute } from './create_scheduled_query_route'; // import { deleteScheduledQueryRoute } from './delete_scheduled_query_route'; -import { findScheduledQueryRoute } from './find_scheduled_query_route'; -import { readScheduledQueryRoute } from './read_scheduled_query_route'; +import { findScheduledQueryGroupRoute } from './find_scheduled_query_group_route'; +import { readScheduledQueryGroupRoute } from './read_scheduled_query_group_route'; // import { updateScheduledQueryRoute } from './update_scheduled_query_route'; -export const initScheduledQueryRoutes = (router: IRouter, context: OsqueryAppContext) => { +export const initScheduledQueryGroupRoutes = (router: IRouter, context: OsqueryAppContext) => { // createScheduledQueryRoute(router); // deleteScheduledQueryRoute(router); - findScheduledQueryRoute(router, context); - readScheduledQueryRoute(router, context); + findScheduledQueryGroupRoute(router, context); + readScheduledQueryGroupRoute(router, context); // updateScheduledQueryRoute(router); }; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts similarity index 65% rename from x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts index 009374f6a2e9..de8125aab5b2 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts @@ -6,28 +6,34 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const readScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const readScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { router.get( { - path: '/internal/osquery/scheduled_query/{id}', + path: '/internal/osquery/scheduled_query_group/{id}', validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const packagePolicyService = osqueryContext.service.getPackagePolicyService(); - // @ts-expect-error update types - const scheduledQuery = await packagePolicyService?.get(savedObjectsClient, request.params.id); + const scheduledQueryGroup = await packagePolicyService?.get( + savedObjectsClient, + // @ts-expect-error update types + request.params.id + ); return response.ok({ - // @ts-expect-error update types - body: scheduledQuery, + body: { item: scheduledQueryGroup }, }); } ); diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts similarity index 92% rename from x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts index efb4f2990e94..2a6e7a33fcdd 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts index d7ea49c6152c..0a527424f9f4 100644 --- a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts +++ b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -14,16 +14,10 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon { path: '/internal/osquery/status', validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const soClient = context.core.savedObjects.client; - const isSuperUser = osqueryContext.security.authc - .getCurrentUser(request) - ?.roles.includes('superuser'); - - if (!isSuperUser) { - return response.ok({ body: undefined }); - } const packageInfo = await osqueryContext.service .getPackageService() diff --git a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts index 92709f92d9e5..603bcad87cf8 100644 --- a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts @@ -23,6 +23,6 @@ export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = { export const usageMetricType: SavedObjectsType = { name: usageMetricSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'agnostic', mappings: usageMetricSavedObjectMappings, }; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts index 9fffb0726dce..2fa9ee04ab53 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts @@ -23,7 +23,7 @@ import { OsqueryFactory } from './factory/types'; export const osquerySearchStrategyProvider = ( data: PluginStart ): ISearchStrategy, StrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + let es: typeof data.search.searchAsInternalUser; return { search: (request, options, deps) => { @@ -32,20 +32,35 @@ export const osquerySearchStrategyProvider = ( } const queryFactory: OsqueryFactory = osqueryFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - return es.search({ ...request, params: dsl }, options, deps).pipe( - map((response) => { - return { - ...response, - ...{ - rawResponse: shimHitsTotal(response.rawResponse), - }, - }; - }), - mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) - ); + + // use internal user for searching .fleet* indicies + es = dsl.index?.includes('fleet') + ? data.search.searchAsInternalUser + : data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return es + .search( + { + ...request, + params: dsl, + }, + options, + deps + ) + .pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); }, cancel: async (id, options, deps) => { - if (es.cancel) { + if (es?.cancel) { return es.cancel(id, options, deps); } }, diff --git a/x-pack/plugins/osquery/server/usage/collector.ts b/x-pack/plugins/osquery/server/usage/collector.ts index 4432592a4e06..b04fc34e5245 100644 --- a/x-pack/plugins/osquery/server/usage/collector.ts +++ b/x-pack/plugins/osquery/server/usage/collector.ts @@ -11,11 +11,12 @@ import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers import { CollectorDependencies, usageSchema, UsageData } from './types'; export type RegisterCollector = (deps: CollectorDependencies) => void; -export async function getInternalSavedObjectsClient(core: CoreSetup) { - return core.getStartServices().then(async ([coreStart]) => { - return coreStart.savedObjects.createInternalRepository(); - }); -} +export const getInternalSavedObjectsClient = async ( + getStartServices: CoreSetup['getStartServices'] +) => { + const [coreStart] = await getStartServices(); + return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); +}; export const registerCollector: RegisterCollector = ({ core, osqueryContext, usageCollection }) => { if (!usageCollection) { @@ -26,7 +27,8 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa schema: usageSchema, isReady: () => true, fetch: async ({ esClient }: CollectorFetchContext): Promise => { - const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core)); + const savedObjectsClient = await getInternalSavedObjectsClient(core.getStartServices); + return { beat_metrics: { usage: await getBeatUsage(esClient), diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 76e26c770cfe..50b980767095 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/painless_lab/tsconfig.json b/x-pack/plugins/painless_lab/tsconfig.json index a869b21e06d4..e0cf386193bb 100644 --- a/x-pack/plugins/painless_lab/tsconfig.json +++ b/x-pack/plugins/painless_lab/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json index 9dc7926bd62e..006c3c53c1be 100644 --- a/x-pack/plugins/remote_clusters/tsconfig.json +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index 96967dc9226c..86d618c57379 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -86,7 +86,7 @@ export class Job { let smallMessage; if (status === PENDING) { smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.pendingStatusReachedText', { - defaultMessage: 'Waiting for job to be processed.', + defaultMessage: 'Waiting for job to process.', }); } else if (status === PROCESSING) { smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.attemptXofY', { @@ -139,8 +139,7 @@ export class Job { getStatusLabel() { return ( <> - {this.getStatus()} - {this.getStatusMessage()} + {this.getStatus()} {this.getStatusMessage()} ); } @@ -184,14 +183,14 @@ export class Job { warnings.push( i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas.', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', }) ); } if (this.max_size_reached) { warnings.push( i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { - defaultMessage: 'Max size reached, contains partial data.', + defaultMessage: 'Your search reached the max size and contains partial data.', }) ); } 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 5c618ba8261f..151519b0b6b8 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 @@ -123,7 +123,7 @@ export class ReportingAPIClient implements IReportingAPI { } return i18n.translate('xpack.reporting.apiClient.unknownError', { - defaultMessage: `Report job {job} failed: Unknown error.`, + defaultMessage: `Report job {job} failed. Error unknown.`, values: { job: jobId }, }); } 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 4ab50750bbc5..e8b9362db752 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 @@ -61,7 +61,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Unable to fetch report info + Unable to fetch report info.
@@ -113,7 +113,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Unable to fetch report info + Unable to fetch 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 89511aaf96a6..926ca6e0b53d 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 @@ -549,6 +549,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 05:01 PM + @@ -562,7 +563,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTextColor euiTextColor--subdued" style={Object {}} > - Waiting for job to be processed. + Waiting for job to process. @@ -1293,7 +1294,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -1562,6 +1563,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 05:01 PM + @@ -2306,7 +2308,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -2575,6 +2577,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 04:19 PM + @@ -3348,7 +3351,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -3617,6 +3620,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:21 PM + @@ -4418,7 +4422,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -4687,6 +4691,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:19 PM + @@ -5460,7 +5465,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -5729,6 +5734,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:19 PM + @@ -6502,7 +6508,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -6771,6 +6777,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:18 PM + @@ -7544,7 +7551,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -7813,6 +7820,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:13 PM + @@ -8586,7 +8594,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -8855,6 +8863,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-09 @ 03:10 PM + @@ -9628,7 +9637,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx index a40f167de5bc..1dccb11dbbbc 100644 --- a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx +++ b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx @@ -12,11 +12,11 @@ import { EuiButtonEmpty } from '@elastic/eui'; import type { ApplicationStart } from 'src/core/public'; import { ILM_POLICY_NAME } from '../../common/constants'; -import { LocatorPublic, SerializableState } from '../shared_imports'; +import { LocatorPublic, SerializableRecord } from '../shared_imports'; interface Props { navigateToUrl: ApplicationStart['navigateToUrl']; - locator: LocatorPublic; + locator: LocatorPublic; } const i18nTexts = { 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 8513558fb89c..7a70286785e4 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.tsx @@ -92,7 +92,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.createdAtInfo', - defaultMessage: 'Created At', + defaultMessage: 'Created at', }), description: info.getCreatedAtLabel(), }, @@ -106,7 +106,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.tzInfo', - defaultMessage: 'Timezone', + defaultMessage: 'Time zone', }), description: info.browserTimezone || NA, }, @@ -116,21 +116,21 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.startedAtInfo', - defaultMessage: 'Started At', + defaultMessage: 'Started at', }), description: info.started_at || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.completedAtInfo', - defaultMessage: 'Completed At', + defaultMessage: 'Completed at', }), description: info.completed_at || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.processedByInfo', - defaultMessage: 'Processed By', + defaultMessage: 'Processed by', }), description: info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, @@ -138,14 +138,14 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', - defaultMessage: 'Content Type', + defaultMessage: 'Content type', }), description: info.content_type || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.sizeInfo', - defaultMessage: 'Size in Bytes', + defaultMessage: 'Size in bytes', }), description: info.size?.toString() || NA, }, @@ -159,7 +159,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', - defaultMessage: 'Max Attempts', + defaultMessage: 'Max attempts', }), description: info.max_attempts?.toString() || NA, }, @@ -173,7 +173,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', - defaultMessage: 'Export Type', + defaultMessage: 'Export type', }), description: info.isDeprecated ? this.props.intl.formatMessage( @@ -207,7 +207,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', - defaultMessage: 'Browser Type', + defaultMessage: 'Browser type', }), description: info.browser_type || NA, }, @@ -293,17 +293,17 @@ class ReportInfoButtonUi extends Component { let message = this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', - defaultMessage: 'See report info', + 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', + 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', + defaultMessage: 'See report info and warnings.', }); } @@ -349,7 +349,7 @@ class ReportInfoButtonUi extends Component { isLoading: false, calloutTitle: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', - defaultMessage: 'Unable to fetch report info', + defaultMessage: 'Unable to fetch report info.', }), info: null, error: err, diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index 02717351e315..a18ceaf151c7 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -19,7 +19,7 @@ import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/pu import { KibanaContext } from './types'; export const useKibana = () => _useKibana(); -export type { SerializableState } from 'src/plugins/kibana_utils/common'; +export type { SerializableRecord } from '@kbn/utility-types'; export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 708b9b1bdbea..e09cee8c3c7c 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -29,7 +29,6 @@ import { ReportingConfig, ReportingSetup } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; -import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots'; import { ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; import { ReportingPluginRouter } from './types'; @@ -237,12 +236,6 @@ export class ReportingCore { .toPromise(); } - public async getScreenshotsObservable(): Promise { - const config = this.getConfig(); - const { browserDriverFactory } = await this.getPluginStartDeps(); - return screenshotsObservableFactory(config.get('capture'), browserDriverFactory); - } - public getEnableScreenshotMode() { const { screenshotMode } = this.getPluginSetupDeps(); return screenshotMode.setScreenshotModeEnabled; diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index 12815cf1b25a..2af56ed9881a 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -11,7 +11,7 @@ import { finalize, map, tap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; -import { ScreenshotResults } from '../../../lib/screenshots'; +import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; function getBase64DecodedSize(value: string) { @@ -24,7 +24,9 @@ function getBase64DecodedSize(value: string) { } export async function generatePngObservableFactory(reporting: ReportingCore) { - const getScreenshots = await reporting.getScreenshotsObservable(); + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const { browserDriverFactory } = await reporting.getPluginStartDeps(); return function generatePngObservable( logger: LevelLogger, @@ -43,7 +45,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup'); let apmBuffer: typeof apm.currentSpan; - const screenshots$ = getScreenshots({ + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urls: [url], conditionalHeaders, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 9b1a1820b002..88b9f8dc95b9 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -11,7 +11,7 @@ import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; -import { ScreenshotResults } from '../../../lib/screenshots'; +import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; import { PdfMaker } from './pdf'; import { getTracker } from './tracker'; @@ -29,7 +29,7 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { export async function generatePdfObservableFactory(reporting: ReportingCore) { const config = reporting.getConfig(); const captureConfig = config.get('capture'); - const getScreenshots = await reporting.getScreenshotsObservable(); + const { browserDriverFactory } = await reporting.getPluginStartDeps(); return function generatePdfObservable( logger: LevelLogger, @@ -48,7 +48,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.endLayout(); tracker.startScreenshots(); - const screenshots$ = getScreenshots({ + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urls, conditionalHeaders, diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts index abfa2b88258f..8cfea1b010df 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts @@ -16,7 +16,7 @@ import { } from '../test_helpers'; import { ReportingRequestHandlerContext } from '../types'; import { ExportTypesRegistry, ReportingStore } from './'; -import { enqueueJobFactory } from './enqueue_job'; +import { enqueueJob } from './enqueue_job'; import { Report } from './store'; describe('Enqueue Job', () => { @@ -72,13 +72,14 @@ describe('Enqueue Job', () => { }); it('returns a Report object', async () => { - const enqueueJob = enqueueJobFactory(mockReporting, logger); const report = await enqueueJob( + mockReporting, + ({} as unknown) as KibanaRequest, + ({} as unknown) as ReportingRequestHandlerContext, + false, 'printablePdf', mockBaseParams, - false, - ({} as unknown) as ReportingRequestHandlerContext, - ({} as unknown) as KibanaRequest + logger ); const { _id, created_at: _created_at, ...snapObj } = report; @@ -117,14 +118,15 @@ describe('Enqueue Job', () => { }); it('provides a default kibana version field for older POST URLs', async () => { - const enqueueJob = enqueueJobFactory(mockReporting, logger); mockBaseParams.version = undefined; const report = await enqueueJob( + mockReporting, + ({} as unknown) as KibanaRequest, + ({} as unknown) as ReportingRequestHandlerContext, + false, 'printablePdf', mockBaseParams, - false, - ({} as unknown) as ReportingRequestHandlerContext, - ({} as unknown) as KibanaRequest + logger ); const { _id, created_at: _created_at, ...snapObj } = report; diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 1c73b0d925ad..998e4edf26a3 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -12,64 +12,53 @@ import { BaseParams, ReportingUser } from '../types'; import { checkParamsVersion, LevelLogger } from './'; import { Report } from './store'; -export type EnqueueJobFn = ( +export async function enqueueJob( + reporting: ReportingCore, + request: KibanaRequest, + context: ReportingRequestHandlerContext, + user: ReportingUser, exportTypeId: string, jobParams: BaseParams, - user: ReportingUser, - context: ReportingRequestHandlerContext, - request: KibanaRequest -) => Promise; - -export function enqueueJobFactory( - reporting: ReportingCore, parentLogger: LevelLogger -): EnqueueJobFn { +): Promise { const logger = parentLogger.clone(['createJob']); - return async function enqueueJob( - exportTypeId: string, - jobParams: BaseParams, - user: ReportingUser, - context: ReportingRequestHandlerContext, - request: KibanaRequest - ) { - const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); + const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); - if (exportType == null) { - throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); - } + if (exportType == null) { + throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); + } - if (!exportType.createJobFnFactory) { - throw new Error(`Export type ${exportTypeId} is not an async job type!`); - } + if (!exportType.createJobFnFactory) { + throw new Error(`Export type ${exportTypeId} is not an async job type!`); + } - const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), - reporting.getStore(), - ]); + const [createJob, store] = await Promise.all([ + exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + reporting.getStore(), + ]); - jobParams.version = checkParamsVersion(jobParams, logger); - const job = await createJob!(jobParams, context, request); + jobParams.version = checkParamsVersion(jobParams, logger); + const job = await createJob!(jobParams, context, request); - // 1. Add the report to ReportingStore to show as pending - const report = await store.addReport( - new Report({ - jobtype: exportType.jobType, - created_by: user ? user.username : false, - payload: job, - meta: { - objectType: jobParams.objectType, - layout: jobParams.layout?.id, - }, - }) - ); - logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); + // 1. Add the report to ReportingStore to show as pending + const report = await store.addReport( + new Report({ + jobtype: exportType.jobType, + created_by: user ? user.username : false, + payload: job, + meta: { + objectType: jobParams.objectType, + layout: jobParams.layout?.id, + }, + }) + ); + logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); - // 2. Schedule the report with Task Manager - const task = await reporting.scheduleTask(report.toReportTaskJSON()); - logger.info( - `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` - ); + // 2. Schedule the report with Task Manager + const task = await reporting.scheduleTask(report.toReportTaskJSON()); + logger.info( + `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` + ); - return report; - }; + return report; } diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index d924b45c1e01..d5ef52d627c6 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -5,12 +5,11 @@ * 2.0. */ -import * as Rx from 'rxjs'; import { LevelLogger } from '../'; import { ConditionalHeaders } from '../../export_types/common'; import { LayoutInstance } from '../layouts'; -export { screenshotsObservableFactory } from './observable'; +export { getScreenshots$ } from './observable'; export interface ScreenshotObservableOpts { logger: LevelLogger; @@ -55,11 +54,3 @@ export interface ScreenshotResults { error?: Error; elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing } - -export type ScreenshotsObservableFn = ({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, -}: ScreenshotObservableOpts) => Rx.Observable; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index dd8aadb49a5b..7458340a4a52 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -32,7 +32,7 @@ import { } from '../../test_helpers'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; -import { screenshotsObservableFactory } from './observable'; +import { getScreenshots$ } from './'; /* * Mocks @@ -67,8 +67,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const result = await getScreenshots$({ + const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index.htm'], conditionalHeaders: {} as ConditionalHeaders, @@ -128,8 +127,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const result = await getScreenshots$({ + const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], conditionalHeaders: {} as ConditionalHeaders, @@ -227,9 +225,8 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { - return await getScreenshots$({ + return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: [ '/welcome/home/start/index2.htm', @@ -322,9 +319,8 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { - return await getScreenshots$({ + return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index.php3?page=./home.php3'], conditionalHeaders: {} as ConditionalHeaders, @@ -354,50 +350,46 @@ describe('Screenshot Observable Pipeline', () => { }); mockLayout.getViewport = () => null; - // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const getScreenshot = async () => { - return await getScreenshots$({ - logger, - urls: ['/welcome/home/start/index.php3?page=./home.php3'], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - }; + const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { + logger, + urls: ['/welcome/home/start/index.php3?page=./home.php3'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 1200, - "left": 0, - "top": 0, - "width": 1800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "base64EncodedData": "allyourBase64", - "description": undefined, - "title": undefined, - }, - ], - "timeRange": undefined, + expect(screenshots).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, }, - ] - `); + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 369267806441..baaf8a4fb38e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -10,12 +10,7 @@ import * as Rx from 'rxjs'; import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; import { HeadlessChromiumDriverFactory } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - ElementsPositionAndAttribute, - ScreenshotObservableOpts, - ScreenshotResults, - ScreenshotsObservableFn, -} from './'; +import { ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults } from './'; import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; @@ -36,117 +31,110 @@ interface ScreenSetupData { error?: Error; } -export function screenshotsObservableFactory( +export function getScreenshots$( captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -): ScreenshotsObservableFn { - return function screenshotsObservable({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, - }: ScreenshotObservableOpts): Rx.Observable { - const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); + browserDriverFactory: HeadlessChromiumDriverFactory, + { logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts +): Rx.Observable { + const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); - const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); - const create$ = browserDriverFactory.createPage( - { viewport: layout.getBrowserViewport(), browserTimezone }, - logger - ); + const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); + const create$ = browserDriverFactory.createPage( + { viewport: layout.getBrowserViewport(), browserTimezone }, + logger + ); - return create$.pipe( - mergeMap(({ driver, exit$ }) => { - apmCreatePage?.end(); - exit$.subscribe({ error: () => apmTrans?.end() }); + return create$.pipe( + mergeMap(({ driver, exit$ }) => { + apmCreatePage?.end(); + exit$.subscribe({ error: () => apmTrans?.end() }); - return Rx.from(urls).pipe( - concatMap((url, index) => { - const setup$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => { - // If we're moving to another page in the app, we'll want to wait for the app to tell us - // it's loaded the next page. - const page = index + 1; - const pageLoadSelector = - page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; + return Rx.from(urls).pipe( + concatMap((url, index) => { + const setup$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => { + // If we're moving to another page in the app, we'll want to wait for the app to tell us + // it's loaded the next page. + const page = index + 1; + const pageLoadSelector = + page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; - return openUrl( - captureConfig, - driver, - url, - pageLoadSelector, - conditionalHeaders, - logger - ); - }), - mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), - mergeMap(async (itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout - const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); - await Promise.all([ - driver.setViewport(viewport, logger), - waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), - ]); - }), - mergeMap(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); + return openUrl( + captureConfig, + driver, + url, + pageLoadSelector, + conditionalHeaders, + logger + ); + }), + mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), + mergeMap(async (itemsCount) => { + // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); + await Promise.all([ + driver.setViewport(viewport, logger), + waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), + ]); + }), + mergeMap(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); - const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); - if (layout.positionElements) { - // position panel elements for print layout - await layout.positionElements(driver, logger); - } - if (apmPositionElements) apmPositionElements.end(); + const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); + if (layout.positionElements) { + // position panel elements for print layout + await layout.positionElements(driver, logger); + } + if (apmPositionElements) apmPositionElements.end(); - await waitForRenderComplete(captureConfig, driver, layout, logger); - }), - mergeMap(async () => { - return await Promise.all([ - getTimeRange(driver, layout, logger), - getElementPositionAndAttributes(driver, layout, logger), - ]).then(([timeRange, elementsPositionAndAttributes]) => ({ - elementsPositionAndAttributes, - timeRange, - })); - }), - catchError((err) => { - checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it + await waitForRenderComplete(captureConfig, driver, layout, logger); + }), + mergeMap(async () => { + return await Promise.all([ + getTimeRange(driver, layout, logger), + getElementPositionAndAttributes(driver, layout, logger), + ]).then(([timeRange, elementsPositionAndAttributes]) => ({ + elementsPositionAndAttributes, + timeRange, + })); + }), + catchError((err) => { + checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it - logger.error(err); - return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); - }) - ); + logger.error(err); + return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); + }) + ); - return setup$.pipe( - takeUntil(exit$), - mergeMap( - async (data: ScreenSetupData): Promise => { - checkPageIsOpen(driver); // re-check that the browser has not closed + return setup$.pipe( + takeUntil(exit$), + mergeMap( + async (data: ScreenSetupData): Promise => { + checkPageIsOpen(driver); // re-check that the browser has not closed - const elements = data.elementsPositionAndAttributes - ? data.elementsPositionAndAttributes - : getDefaultElementPosition(layout.getViewport(1)); - const screenshots = await getScreenshots(driver, layout, elements, logger); - const { timeRange, error: setupError } = data; - return { - timeRange, - screenshots, - error: setupError, - elementsPositionAndAttributes: elements, - }; - } - ) - ); - }), - take(urls.length), - toArray() - ); - }), - first() - ); - }; + const elements = data.elementsPositionAndAttributes + ? data.elementsPositionAndAttributes + : getDefaultElementPosition(layout.getViewport(1)); + const screenshots = await getScreenshots(driver, layout, elements, logger); + const { timeRange, error: setupError } = data; + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; + } + ) + ); + }), + take(urls.length), + toArray() + ); + }), + first() + ); } /* diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 3d482d4f84d5..57324830af4a 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -13,7 +13,7 @@ import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/exe import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../lib/tasks'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { HandlerErrorFunction } from './types'; const API_BASE_URL_V1 = '/api/reporting/v1'; @@ -36,7 +36,6 @@ export function registerGenerateCsvFromSavedObjectImmediate( parentLogger: Logger ) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new @@ -63,47 +62,50 @@ export function registerGenerateCsvFromSavedObjectImmediate( tags: kibanaAccessControlTags, }, }, - userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone(['csv_searchsource_immediate']); - const runTaskFn = runTaskFnFactory(reporting, logger); + authorizedUserPreRouting( + reporting, + async (_user, context, req: CsvFromSavedObjectRequest, res) => { + const logger = parentLogger.clone(['csv_searchsource_immediate']); + const runTaskFn = runTaskFnFactory(reporting, logger); - try { - let buffer = Buffer.from(''); - const stream = new Writable({ - write(chunk, encoding, callback) { - buffer = Buffer.concat([ - buffer, - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), - ]); - callback(); - }, - }); + try { + let buffer = Buffer.from(''); + const stream = new Writable({ + write(chunk, encoding, callback) { + buffer = Buffer.concat([ + buffer, + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), + ]); + callback(); + }, + }); - const { - content_type: jobOutputContentType, - size: jobOutputSize, - }: TaskRunResult = await runTaskFn(null, req.body, context, stream, req); - stream.end(); - const jobOutputContent = buffer.toString(); + const { + content_type: jobOutputContentType, + size: jobOutputSize, + }: TaskRunResult = await runTaskFn(null, req.body, context, stream, req); + stream.end(); + const jobOutputContent = buffer.toString(); - logger.info(`Job output size: ${jobOutputSize} bytes`); + logger.info(`Job output size: ${jobOutputSize} bytes`); - // convert null to undefined so the value can be sent to h.response() - if (jobOutputContent === null) { - logger.warn('CSV Job Execution created empty content result'); - } + // convert null to undefined so the value can be sent to h.response() + if (jobOutputContent === null) { + logger.warn('CSV Job Execution created empty content result'); + } - return res.ok({ - body: jobOutputContent || '', - headers: { - 'content-type': jobOutputContentType ? jobOutputContentType : [], - 'accept-ranges': 'none', - }, - }); - } catch (err) { - logger.error(err); - return handleError(res, err); + return res.ok({ + body: jobOutputContent || '', + headers: { + 'content-type': jobOutputContentType ? jobOutputContentType : [], + 'accept-ranges': 'none', + }, + }); + } catch (err) { + logger.error(err); + return handleError(res, err); + } } - }) + ) ); } diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index 37361fc91392..1652f91cd11a 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -88,7 +88,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), removeEventListener: jest.fn(), removeAllListeners: jest.fn(), close: jest.fn(), @@ -110,7 +110,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(logs), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(logs), 0), removeEventListener: jest.fn(), removeAllListeners: jest.fn(), close: jest.fn(), @@ -146,7 +146,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => { + addEventListener: (_e: string, cb: any) => { setTimeout(() => cb(devtoolMessage), 0); setTimeout(() => cb(fontNotFoundMessage), 0); }, @@ -186,7 +186,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => { + addEventListener: (_e: string, cb: any) => { setTimeout(() => cb(fontNotFoundMessage), 0); }, removeEventListener: jest.fn(), @@ -209,17 +209,16 @@ describe('POST /diagnose/browser', () => { .post('/api/reporting/diagnose/browser') .expect(200) .then(({ body }) => { - expect(body).toMatchInlineSnapshot(` - Object { - "help": Array [ - "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", - ], - "logs": "Could not find the default font - Browser exited abnormally during startup - ", - "success": false, - } + const helpArray = [...body.help]; + helpArray.sort(); + expect(helpArray).toMatchInlineSnapshot(` + Array [ + "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", + ] `); + expect(body.logs).toMatch(/Could not find the default font/); + expect(body.logs).toMatch(/Browser exited abnormally during startup/); + expect(body.success).toBe(false); }); }); @@ -242,7 +241,7 @@ describe('POST /diagnose/browser', () => { })); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), removeEventListener: jest.fn(), removeAllListeners: createInterfaceListenersMock, close: createInterfaceCloseMock, diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 25e60c65676d..268cabd9be55 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -10,7 +10,7 @@ import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; const logsToHelpMap = { @@ -47,14 +47,13 @@ const logsToHelpMap = { export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => { const { router } = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); router.post( { path: `${API_DIAGNOSE_URL}/browser`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => { try { const logs = await browserStartLogs(reporting, logger).toPromise(); const knownIssues = Object.keys(logsToHelpMap) as Array; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 109849aa302f..93677409693f 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -11,7 +11,7 @@ import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; @@ -27,7 +27,6 @@ const numberToByteSizeValue = (value: number | ByteSizeValue) => { export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; router.post( @@ -35,7 +34,7 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) path: `${API_DIAGNOSE_URL}/config`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => { const warnings = []; const { asInternalUser: elasticsearchClient } = await reporting.getEsClient(); const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 7405e8cff897..765e0a2a4e8a 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -13,12 +13,11 @@ import { omitBlockedHeaders } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; router.post( @@ -26,7 +25,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log path: `${API_DIAGNOSE_URL}/screenshot`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, req, res) => { const generatePngObservable = await generatePngObservableFactory(reporting); const config = reporting.getConfig(); const decryptedHeaders = req.headers as Record; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 69b3f216886e..c519616cda5f 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -10,7 +10,7 @@ import rison from 'rison-node'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { BaseParams } from '../types'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -21,7 +21,6 @@ export function registerGenerateFromJobParams( handleError: HandlerErrorFunction ) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new @@ -41,7 +40,7 @@ export function registerGenerateFromJobParams( }, options: { tags: kibanaAccessControlTags }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { let jobParamsRison: null | string = null; if (req.body) { diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index ce6d1a2f2641..4082084c82fb 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -10,7 +10,7 @@ import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; -import { enqueueJobFactory } from '../lib/enqueue_job'; +import { enqueueJob } from '../lib/enqueue_job'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; import { HandlerFunction } from './types'; @@ -42,8 +42,15 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo } try { - const enqueueJob = enqueueJobFactory(reporting, logger); - const report = await enqueueJob(exportTypeId, jobParams, user, context, req); + const report = await enqueueJob( + reporting, + req, + context, + user, + exportTypeId, + jobParams, + logger + ); // return task manager's task information and the download URL const downloadBaseUrl = getDownloadBaseUrl(reporting); diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 37557c3afb0c..ad0aac121106 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -10,12 +10,9 @@ import Boom from '@hapi/boom'; import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { jobsQueryFactory } from './lib/jobs_query'; -import { - deleteJobResponseHandlerFactory, - downloadJobResponseHandlerFactory, -} from './lib/job_response_handler'; +import { deleteJobResponseHandler, downloadJobResponseHandler } from './lib/job_response_handler'; const MAIN_ENTRY = `${API_BASE_URL}/jobs`; @@ -25,8 +22,8 @@ const handleUnavailable = (res: any) => { export function registerJobInfoRoutes(reporting: ReportingCore) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; + const jobsQuery = jobsQueryFactory(reporting); // list jobs in the queue, paginated router.get( @@ -40,7 +37,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -53,7 +50,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; - const jobsQuery = jobsQueryFactory(reporting); const results = await jobsQuery.list(jobTypes, user, page, size, jobIds); return res.ok({ @@ -71,7 +67,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { path: `${MAIN_ENTRY}/count`, validate: false, }, - userHandler(async (user, context, _req, res) => { + authorizedUserPreRouting(reporting, async (user, context, _req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -81,7 +77,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const jobsQuery = jobsQueryFactory(reporting); const count = await jobsQuery.count(jobTypes, user); return res.ok({ @@ -103,7 +98,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return res.custom({ statusCode: 503 }); @@ -114,7 +109,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const jobsQuery = jobsQueryFactory(reporting); const result = await jobsQuery.get(user, docId); if (!result) { @@ -137,8 +131,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ); // trigger a download of the output from a job - const downloadResponseHandler = downloadJobResponseHandlerFactory(reporting); - router.get( { path: `${MAIN_ENTRY}/download/{docId}`, @@ -149,7 +141,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }, options: { tags: [ROUTE_TAG_CAN_REDIRECT] }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -160,12 +152,11 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - return downloadResponseHandler(res, jobTypes, user, { docId }); + return downloadJobResponseHandler(reporting, res, jobTypes, user, { docId }); }) ); // allow a report to be deleted - const deleteResponseHandler = deleteJobResponseHandlerFactory(reporting); router.delete( { path: `${MAIN_ENTRY}/delete/{docId}`, @@ -175,7 +166,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -186,7 +177,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - return deleteResponseHandler(res, jobTypes, user, { docId }); + return deleteJobResponseHandler(reporting, res, jobTypes, user, { docId }); }) ); } diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 16ef9e6d5bc1..0cff81539e52 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -11,7 +11,7 @@ import { ReportingCore } from '../../'; import { ReportingInternalSetup } from '../../core'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../types'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './authorized_user_pre_routing'; let mockCore: ReportingCore; const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); @@ -46,11 +46,10 @@ describe('authorized_user_pre_routing', function () { ...mockCore.pluginSetupDeps, security: undefined, // disable security } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting((user: unknown) => { + authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -70,11 +69,10 @@ describe('authorized_user_pre_routing', function () { }, }, // disable security } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting((user: unknown) => { + authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -93,11 +91,10 @@ describe('authorized_user_pre_routing', function () { authc: { getCurrentUser: () => null }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; - const requestHandler = authorizedUserPreRouting(mockHandler); + const requestHandler = authorizedUserPreRouting(mockCore, mockHandler); const mockResponseFactory = getMockResponseFactory(); expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ @@ -126,14 +123,13 @@ describe('authorized_user_pre_routing', function () { authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = getMockResponseFactory(); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; expect( - authorizedUserPreRouting(mockHandler)( + authorizedUserPreRouting(mockCore, mockHandler)( getMockContext(), getMockRequest(), mockResponseFactory @@ -153,10 +149,9 @@ describe('authorized_user_pre_routing', function () { }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = getMockResponseFactory(); - authorizedUserPreRouting((user) => { + authorizedUserPreRouting(mockCore, (user) => { expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); done(); return Promise.resolve({ status: 200, options: {} }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 846d8c28a537..2c6a01e61548 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -8,12 +8,13 @@ import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../security/server'; import { ReportingCore } from '../../core'; -import { getUserFactory } from './get_user'; +import { getUser } from './get_user'; import type { ReportingRequestHandlerContext } from '../../types'; const superuserRole = 'superuser'; type ReportingRequestUser = AuthenticatedUser | false; + export type RequestHandlerUser = RequestHandler< P, Q, @@ -23,43 +24,40 @@ export type RequestHandlerUser = RequestHandler< ? (user: ReportingRequestUser, ...a: U) => R : never; -export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - reporting: ReportingCore -) { +export const authorizedUserPreRouting = ( + reporting: ReportingCore, + handler: RequestHandlerUser +): RequestHandler => { const { logger, security } = reporting.getPluginSetupDeps(); - const getUser = getUserFactory(security); - return ( - handler: RequestHandlerUser - ): RequestHandler => { - return (context, req, res) => { - try { - let user: ReportingRequestUser = false; - if (security && security.license.isEnabled()) { - // find the authenticated user, or null if security is not enabled - user = getUser(req); - if (!user) { - // security is enabled but the user is null - return res.unauthorized({ body: `Sorry, you aren't authenticated` }); - } + + return (context, req, res) => { + try { + let user: ReportingRequestUser = false; + if (security && security.license.isEnabled()) { + // find the authenticated user, or null if security is not enabled + user = getUser(req, security); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); } + } - const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles(); - if (user && deprecatedAllowedRoles !== false) { - // check allowance with the configured set of roleas + "superuser" - const allowedRoles = deprecatedAllowedRoles || []; - const authorizedRoles = [superuserRole, ...allowedRoles]; + const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles(); + if (user && deprecatedAllowedRoles !== false) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = deprecatedAllowedRoles || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - // user's roles do not allow - return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); - } + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); } - - return handler(user, context, req, res); - } catch (err) { - logger.error(err); - return res.custom({ statusCode: 500 }); } - }; + + return handler(user, context, req, res); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }; }; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/lib/get_user.ts index 31d2009346b5..cc5c97c7a355 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_user.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_user.ts @@ -8,8 +8,6 @@ import { KibanaRequest } from 'kibana/server'; import { SecurityPluginSetup } from '../../../../security/server'; -export function getUserFactory(security?: SecurityPluginSetup) { - return (request: KibanaRequest) => { - return security?.authc.getCurrentUser(request) ?? false; - }; +export function getUser(request: KibanaRequest, security?: SecurityPluginSetup) { + return security?.authc.getCurrentUser(request) ?? false; } diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 9f90be09bccb..419f2e118f03 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -16,93 +16,85 @@ interface JobResponseHandlerParams { docId: string; } -interface JobResponseHandlerOpts { - excludeContent?: boolean; -} - -export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { +export async function downloadJobResponseHandler( + reporting: ReportingCore, + res: typeof kibanaResponseFactory, + validJobTypes: string[], + user: ReportingUser, + params: JobResponseHandlerParams +) { const jobsQuery = jobsQueryFactory(reporting); const getDocumentPayload = getDocumentPayloadFactory(reporting); - - return async function jobResponseHandler( - res: typeof kibanaResponseFactory, - validJobTypes: string[], - user: ReportingUser, - params: JobResponseHandlerParams, - opts: JobResponseHandlerOpts = {} - ) { - try { - const { docId } = params; - - const doc = await jobsQuery.get(user, docId); - if (!doc) { - return res.notFound(); - } - - if (!validJobTypes.includes(doc.jobtype)) { - return res.unauthorized({ - body: `Sorry, you are not authorized to download ${doc.jobtype} reports`, - }); - } - - const payload = await getDocumentPayload(doc); - - if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { - return res.badRequest({ - body: `Unsupported content-type of ${payload.contentType} specified by job output`, - }); - } - - return res.custom({ - body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, - statusCode: payload.statusCode, - headers: { - ...payload.headers, - 'content-type': payload.contentType || '', - }, - }); - } catch (err) { - const { logger } = reporting.getPluginSetupDeps(); - logger.error(err); - } - }; -} - -export function deleteJobResponseHandlerFactory(reporting: ReportingCore) { - const jobsQuery = jobsQueryFactory(reporting); - - return async function deleteJobResponseHander( - res: typeof kibanaResponseFactory, - validJobTypes: string[], - user: ReportingUser, - params: JobResponseHandlerParams - ) { + try { const { docId } = params; - const doc = await jobsQuery.get(user, docId); + const doc = await jobsQuery.get(user, docId); if (!doc) { return res.notFound(); } - const { jobtype: jobType } = doc; - - if (!validJobTypes.includes(jobType)) { + if (!validJobTypes.includes(doc.jobtype)) { return res.unauthorized({ - body: `Sorry, you are not authorized to delete ${jobType} reports`, + body: `Sorry, you are not authorized to download ${doc.jobtype} reports`, }); } - try { - const docIndex = doc.index; - await jobsQuery.delete(docIndex, docId); - return res.ok({ - body: { deleted: true }, - }); - } catch (error) { - return res.customError({ - statusCode: error.statusCode, - body: error.message, + const payload = await getDocumentPayload(doc); + + if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${payload.contentType} specified by job output`, }); } - }; + + return res.custom({ + body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, + statusCode: payload.statusCode, + headers: { + ...payload.headers, + 'content-type': payload.contentType || '', + }, + }); + } catch (err) { + const { logger } = reporting.getPluginSetupDeps(); + logger.error(err); + } +} + +export async function deleteJobResponseHandler( + reporting: ReportingCore, + res: typeof kibanaResponseFactory, + validJobTypes: string[], + user: ReportingUser, + params: JobResponseHandlerParams +) { + const jobsQuery = jobsQueryFactory(reporting); + + const { docId } = params; + const doc = await jobsQuery.get(user, docId); + + if (!doc) { + return res.notFound(); + } + + const { jobtype: jobType } = doc; + + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to delete ${jobType} reports`, + }); + } + + try { + const docIndex = doc.index; + await jobsQuery.delete(docIndex, docId); + return res.ok({ + body: { deleted: true }, + }); + } catch (error) { + return res.customError({ + statusCode: error.statusCode, + body: error.message, + }); + } } diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 406fe9965b8a..3e5845056572 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index 6885081ce4bd..fbe323b2549e 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 16e4b8f3e01e..ef9a3252c41d 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -11,9 +11,11 @@ It also exposes a rule data client that will create or update the index stream t By default, these indices will be prefixed with `.alerts`. To change this, for instance to support legacy multitenancy, set the following configuration option: ```yaml -xpack.ruleRegistry.index: '.kibana-alerts' +xpack.ruleRegistry.index: 'myAlerts' ``` +The above produces an alerts index prefixed `.alerts-myAlerts`. + To disable writing entirely: ```yaml @@ -120,11 +122,11 @@ The following fields are defined in the technical field component template and s - `event.kind`: signal (for the changeable alert document), state (for the state changes of the alert, e.g. when it opens, recovers, or changes in severity), or metric (individual evaluations that might be related to an alert). - `event.action`: the reason for the event. This might be `open`, `close`, `active`, or `evaluate`. - `tags`: tags attached to the alert. Right now they are copied over from the rule. -- `rule.id`: the identifier of the rule type, e.g. `apm.transaction_duration` -- `rule.uuid`: the saved objects id of the rule. -- `rule.name`: the name of the rule (as specified by the user). -- `rule.category`: the name of the rule type (as defined by the rule type producer) -- `kibana.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`... +- `kibana.alert.rule.rule_type_id`: the identifier of the rule type, e.g. `apm.transaction_duration` +- `kibana.alert.rule.uuid`: the saved objects id of the rule. +- `kibana.alert.rule.name`: the name of the rule (as specified by the user). +- `kibana.alert.rule.category`: the name of the rule type (as defined by the rule type producer) +- `kibana.alert.rule.consumer`: the feature which produced the alert (inherited from the rule producer field). Usually a Kibana feature id like `apm`, `siem`... - `kibana.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.alert.status`: the status of the alert. Can be `open` or `closed`. diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index 11e572260d13..eb8d88cf697b 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -14,14 +14,11 @@ export const technicalRuleFieldMap = { Fields.TIMESTAMP, Fields.EVENT_KIND, Fields.EVENT_ACTION, - Fields.RULE_UUID, - Fields.RULE_ID, - Fields.RULE_NAME, - Fields.RULE_CATEGORY, Fields.TAGS ), - [Fields.ALERT_OWNER]: { type: 'keyword', required: true }, - [Fields.ALERT_PRODUCER]: { type: 'keyword' }, + [Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true }, + [Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true }, + [Fields.ALERT_RULE_PRODUCER]: { type: 'keyword' }, [Fields.SPACE_IDS]: { type: 'keyword', array: true, required: true }, [Fields.ALERT_UUID]: { type: 'keyword' }, [Fields.ALERT_ID]: { type: 'keyword' }, @@ -33,11 +30,6 @@ export const technicalRuleFieldMap = { [Fields.ALERT_STATUS]: { type: 'keyword' }, [Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, - [Fields.CONSUMERS]: { - type: 'keyword', - array: true, - required: false, - }, [Fields.VERSION]: { type: 'keyword', array: false, @@ -93,9 +85,19 @@ export const technicalRuleFieldMap = { array: false, required: false, }, - [Fields.ALERT_RULE_CONSUMERS]: { + [Fields.ALERT_RULE_CATEGORY]: { type: 'keyword', - array: true, + array: false, + required: false, + }, + [Fields.ALERT_RULE_UUID]: { + type: 'keyword', + array: false, + required: false, + }, + [Fields.ALERT_RULE_ID]: { + type: 'keyword', + array: false, required: false, }, [Fields.ALERT_RULE_CREATED_AT]: { @@ -123,11 +125,6 @@ export const technicalRuleFieldMap = { array: false, required: false, }, - [Fields.ALERT_RULE_ID]: { - type: 'keyword', - array: false, - required: false, - }, [Fields.ALERT_RULE_INTERVAL]: { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts index 5c954a31e79a..47b00fb1348e 100644 --- a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts +++ b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from '@kbn/rule-data-utils/target/technical_field_names'; +export * from '@kbn/rule-data-utils'; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 2a7419b20570..a67c03abe8b3 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -8,13 +8,20 @@ import Boom from '@hapi/boom'; import { PublicMethodsOf } from '@kbn/utility-types'; import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { - mapConsumerToIndexName, - isValidFeatureId, - getSafeSortIds, +import type { + getEsQueryConfig as getEsQueryConfigTyped, + getSafeSortIds as getSafeSortIdsTyped, + isValidFeatureId as isValidFeatureIdTyped, + mapConsumerToIndexName as mapConsumerToIndexNameTyped, STATUS_VALUES, - getEsQueryConfig, -} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +} from '@kbn/rule-data-utils'; +import { + getEsQueryConfig as getEsQueryConfigNonTyped, + getSafeSortIds as getSafeSortIdsNonTyped, + isValidFeatureId as isValidFeatureIdNonTyped, + mapConsumerToIndexName as mapConsumerToIndexNameNonTyped, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { InlineScript, QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { AlertTypeParams, AlertingAuthorizationFilterType } from '../../../alerting/server'; @@ -29,22 +36,31 @@ import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events'; import { AuditLogger } from '../../../security/server'; import { ALERT_STATUS, - ALERT_OWNER, - RULE_ID, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +const getEsQueryConfig: typeof getEsQueryConfigTyped = getEsQueryConfigNonTyped; +const getSafeSortIds: typeof getSafeSortIdsTyped = getSafeSortIdsNonTyped; +const isValidFeatureId: typeof isValidFeatureIdTyped = isValidFeatureIdNonTyped; +const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerToIndexNameNonTyped; + // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & { [K in Props]-?: NonNullable }; type AlertType = NonNullableProps< ParsedTechnicalFields, - typeof RULE_ID | typeof ALERT_OWNER | typeof SPACE_IDS + typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS >; const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => { - return source?.[RULE_ID] != null && source?.[ALERT_OWNER] != null && source?.[SPACE_IDS] != null; + return ( + source?.[ALERT_RULE_TYPE_ID] != null && + source?.[ALERT_RULE_CONSUMER] != null && + source?.[SPACE_IDS] != null + ); }; export interface ConstructorOptions { logger: Logger; @@ -121,7 +137,10 @@ export class AlertsClient { _id: string; // this is typed kind of crazy to fit the output of es api response to this _source?: - | { [RULE_ID]?: string | null | undefined; [ALERT_OWNER]?: string | null | undefined } + | { + [ALERT_RULE_TYPE_ID]?: string | null | undefined; + [ALERT_RULE_CONSUMER]?: string | null | undefined; + } | null | undefined; }>, @@ -132,16 +151,16 @@ export class AlertsClient { hitIds: [hit._id, ...acc.hitIds], ownersAndRuleTypeIds: [ { - [RULE_ID]: hit?._source?.[RULE_ID], - [ALERT_OWNER]: hit?._source?.[ALERT_OWNER], + [ALERT_RULE_TYPE_ID]: hit?._source?.[ALERT_RULE_TYPE_ID], + [ALERT_RULE_CONSUMER]: hit?._source?.[ALERT_RULE_CONSUMER], }, ], }), { hitIds: [], ownersAndRuleTypeIds: [] } as { hitIds: string[]; ownersAndRuleTypeIds: Array<{ - [RULE_ID]: string | null | undefined; - [ALERT_OWNER]: string | null | undefined; + [ALERT_RULE_TYPE_ID]: string | null | undefined; + [ALERT_RULE_CONSUMER]: string | null | undefined; }>; } ); @@ -150,8 +169,8 @@ export class AlertsClient { return Promise.all( ownersAndRuleTypeIds.map((hit) => { - const alertOwner = hit?.[ALERT_OWNER]; - const ruleId = hit?.[RULE_ID]; + const alertOwner = hit?.[ALERT_RULE_CONSUMER]; + const ruleId = hit?.[ALERT_RULE_TYPE_ID]; if (hit != null && assertString(alertOwner) && assertString(ruleId)) { return this.authorization.ensureAuthorized({ ruleTypeId: ruleId, @@ -322,7 +341,7 @@ export class AlertsClient { AlertingAuthorizationEntity.Alert, { type: AlertingAuthorizationFilterType.ESDSL, - fieldNames: { consumer: ALERT_OWNER, ruleTypeId: RULE_ID }, + fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, }, operation ); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index 97a19935fa78..a6d42853531d 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -77,8 +82,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -134,8 +139,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -180,8 +185,8 @@ describe('bulkUpdate()', () => { _id: successfulAuthzHit, _index: indexName, _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -190,8 +195,8 @@ describe('bulkUpdate()', () => { _id: unsuccessfulAuthzHit, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -267,8 +272,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -327,8 +332,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -388,8 +393,8 @@ describe('bulkUpdate()', () => { _id: successfulAuthzHit, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -398,8 +403,8 @@ describe('bulkUpdate()', () => { _id: unsuccessfulAuthzHit, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 651d728b1983..c8d0d18dfd37 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -85,9 +90,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -100,13 +105,13 @@ describe('get()', () => { const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); expect(result).toMatchInlineSnapshot(` Object { - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "kibana.alert.status": "open", "kibana.space_ids": Array [ "test_default_space_id", ], "message": "hello world 1", - "rule.id": "apm.error_rate", } `); expect(esClientMock.search).toHaveBeenCalledTimes(1); @@ -184,9 +189,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -235,8 +240,8 @@ describe('get()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -307,9 +312,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -330,13 +335,13 @@ describe('get()', () => { expect(result).toMatchInlineSnapshot(` Object { - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "kibana.alert.status": "open", "kibana.space_ids": Array [ "test_default_space_id", ], "message": "hello world 1", - "rule.id": "apm.error_rate", } `); }); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 435b6e310ffd..0aaab2005271 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -82,9 +87,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - [RULE_ID]: 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -168,9 +173,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -242,8 +247,8 @@ describe('update()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -323,9 +328,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -383,9 +388,9 @@ describe('update()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts index 073a48248f89..372fb0966125 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts @@ -6,13 +6,11 @@ */ import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, ALERT_RULE_RISK_SCORE, - ALERT_RULE_SEVERITY, ALERT_STATUS, - CONSUMERS, ECS_VERSION, - RULE_ID, + ALERT_RULE_TYPE_ID, SPACE_IDS, TIMESTAMP, VERSION, @@ -28,14 +26,12 @@ import { requestMock, serverMock } from './__mocks__/server'; const getMockAlert = (): ParsedTechnicalFields => ({ [TIMESTAMP]: '2021-06-21T21:33:05.713Z', [ECS_VERSION]: '1.0.0', - [CONSUMERS]: [], [VERSION]: '7.13.0', - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [ALERT_RULE_RISK_SCORE]: 20, [SPACE_IDS]: ['fake-space-id'], - [ALERT_RULE_SEVERITY]: 'warning', }); describe('getAlertByIdRoute', () => { diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts index 3e3bde7429fe..f3b0b9181c60 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { validFeatureIds } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { validFeatureIds } from '@kbn/rule-data-utils'; import { RacRequestHandlerContext } from '../types'; import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 277121074f7f..ef09dcc7550a 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -7,7 +7,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; -import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { ValidFeatureId } from '@kbn/rule-data-utils'; import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index f81340889e4b..e10b58a1c60c 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -6,7 +6,7 @@ */ import { ClusterPutComponentTemplate } from '@elastic/elasticsearch/api/requestParams'; import { estypes } from '@elastic/elasticsearch'; -import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { ValidFeatureId } from '@kbn/rule-data-utils'; import { ElasticsearchClient, Logger } from 'kibana/server'; import { get, isEmpty } from 'lodash'; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 037efadabd8d..efcc56a1b951 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -23,8 +23,8 @@ import { ALERT_STATUS, EVENT_ACTION, EVENT_KIND, - RULE_ID, - ALERT_OWNER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_CONSUMER, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { createRuleDataClientMock } from '../rule_data_client/create_rule_data_client_mock'; @@ -131,16 +131,16 @@ describe('createLifecycleExecutor', () => { { fields: { [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, }, { fields: { [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, }, @@ -229,8 +229,8 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, @@ -239,8 +239,8 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, @@ -335,7 +335,7 @@ const createDefaultAlertExecutorOptions = < ActionGroupIds extends string = '' >({ alertId = 'ALERT_ID', - ruleName = 'RULE_NAME', + ruleName = 'ALERT_RULE_NAME', params, state, createdAt = new Date(), diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 23ae24cb91bc..7a00457f2c4e 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -29,9 +29,9 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, - ALERT_OWNER, - RULE_ID, - RULE_UUID, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, TIMESTAMP, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; @@ -155,8 +155,8 @@ export const createLifecycleExecutor = ( currentAlerts[id] = { ...fields, [ALERT_ID]: id, - [RULE_ID]: rule.ruleTypeId, - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, + [ALERT_RULE_CONSUMER]: rule.consumer, }; return alertInstanceFactory(id); }, @@ -197,7 +197,7 @@ export const createLifecycleExecutor = ( filter: [ { term: { - [RULE_UUID]: ruleExecutorData[RULE_UUID], + [ALERT_RULE_UUID]: ruleExecutorData[ALERT_RULE_UUID], }, }, { @@ -229,8 +229,8 @@ export const createLifecycleExecutor = ( alertsDataMap[alertId] = { ...fields, [ALERT_ID]: alertId, - [RULE_ID]: rule.ruleTypeId, - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, + [ALERT_RULE_CONSUMER]: rule.consumer, }; }); } @@ -247,7 +247,7 @@ export const createLifecycleExecutor = ( ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'signal', - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_CONSUMER]: rule.consumer, [ALERT_ID]: alertId, } as ParsedTechnicalFields; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index c1358da97e95..346918712212 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -6,15 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - ALERT_DURATION, - ALERT_ID, - ALERT_OWNER, - ALERT_PRODUCER, - ALERT_START, - ALERT_STATUS, - ALERT_UUID, -} from '@kbn/rule-data-utils'; +import { ALERT_DURATION, ALERT_STATUS, ALERT_UUID } from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging/target/mocks'; import { castArray, omit, mapValues } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; @@ -197,19 +189,19 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", - "${ALERT_DURATION}": 0, - "${ALERT_ID}": "opbeans-java", - "${ALERT_OWNER}": "consumer", - "${ALERT_PRODUCER}": "producer", - "${ALERT_START}": "2021-06-16T09:01:00.000Z", - "${ALERT_STATUS}": "open", + "kibana.alert.duration.us": 0, + "kibana.alert.id": "opbeans-java", + "kibana.alert.rule.category": "ruleTypeName", + "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.name": "name", + "kibana.alert.rule.producer": "producer", + "kibana.alert.rule.rule_type_id": "ruleTypeId", + "kibana.alert.rule.uuid": "alertId", + "kibana.alert.start": "2021-06-16T09:01:00.000Z", + "kibana.alert.status": "open", "kibana.space_ids": Array [ "spaceId", ], - "rule.category": "ruleTypeName", - "rule.id": "ruleTypeId", - "rule.name": "name", - "rule.uuid": "alertId", "service.name": "opbeans-java", "tags": Array [ "tags", @@ -219,19 +211,19 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", - "${ALERT_DURATION}": 0, - "${ALERT_ID}": "opbeans-node", - "${ALERT_OWNER}": "consumer", - "${ALERT_PRODUCER}": "producer", - "${ALERT_START}": "2021-06-16T09:01:00.000Z", - "${ALERT_STATUS}": "open", + "kibana.alert.duration.us": 0, + "kibana.alert.id": "opbeans-node", + "kibana.alert.rule.category": "ruleTypeName", + "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.name": "name", + "kibana.alert.rule.producer": "producer", + "kibana.alert.rule.rule_type_id": "ruleTypeId", + "kibana.alert.rule.uuid": "alertId", + "kibana.alert.start": "2021-06-16T09:01:00.000Z", + "kibana.alert.status": "open", "kibana.space_ids": Array [ "spaceId", ], - "rule.category": "ruleTypeName", - "rule.id": "ruleTypeId", - "rule.name": "name", - "rule.uuid": "alertId", "service.name": "opbeans-node", "tags": Array [ "tags", diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 50e5b224f01d..caf14e8ba300 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_ID } from '@kbn/rule-data-utils'; import { CreatePersistenceRuleTypeFactory } from './persistence_types'; export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({ diff --git a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts index 866eb5f882fe..13f0b27e85c3 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts @@ -7,30 +7,30 @@ import { AlertExecutorOptions } from '../../../alerting/server'; import { - ALERT_PRODUCER, - RULE_CATEGORY, - RULE_ID, - RULE_NAME, - RULE_UUID, + ALERT_RULE_PRODUCER, + ALERT_RULE_CATEGORY, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, + ALERT_RULE_UUID, TAGS, } from '../../common/technical_rule_data_field_names'; export interface RuleExecutorData { - [RULE_CATEGORY]: string; - [RULE_ID]: string; - [RULE_UUID]: string; - [RULE_NAME]: string; - [ALERT_PRODUCER]: string; + [ALERT_RULE_CATEGORY]: string; + [ALERT_RULE_TYPE_ID]: string; + [ALERT_RULE_UUID]: string; + [ALERT_RULE_NAME]: string; + [ALERT_RULE_PRODUCER]: string; [TAGS]: string[]; } export function getRuleData(options: AlertExecutorOptions) { return { - [RULE_ID]: options.rule.ruleTypeId, - [RULE_UUID]: options.alertId, - [RULE_CATEGORY]: options.rule.ruleTypeName, - [RULE_NAME]: options.rule.name, + [ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId, + [ALERT_RULE_UUID]: options.alertId, + [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, + [ALERT_RULE_NAME]: options.rule.name, [TAGS]: options.tags, - [ALERT_PRODUCER]: options.rule.producer, + [ALERT_RULE_PRODUCER]: options.rule.producer, }; } diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index f6253e441da3..769c9f81f8ce 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/runtime_fields/tsconfig.json b/x-pack/plugins/runtime_fields/tsconfig.json index a1efe4c9cf2d..5dc704ec5769 100644 --- a/x-pack/plugins/runtime_fields/tsconfig.json +++ b/x-pack/plugins/runtime_fields/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/saved_objects_tagging/tsconfig.json b/x-pack/plugins/saved_objects_tagging/tsconfig.json index d00156ad1277..608cdb2c793c 100644 --- a/x-pack/plugins/saved_objects_tagging/tsconfig.json +++ b/x-pack/plugins/saved_objects_tagging/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json index f8ac3a61f781..c53c65b812a4 100644 --- a/x-pack/plugins/searchprofiler/tsconfig.json +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 6c3fd1851a8c..ea03b9dbb647 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/security_solution/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index 1c42ab3a6fd2..c1d281eccb1f 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -7,7 +7,7 @@ import { DslQuery, Filter } from '@kbn/es-query'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export type ESQuery = | ESRangeQuery diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 10ebae84365f..f5cbc65effd8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -54,7 +54,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 88, + row: 90, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 7d833b134ddd..a6043123ce0a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -14,11 +14,9 @@ import { getNewOverrideRule, } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -223,8 +221,6 @@ describe('Custom detection rules creation', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 677a9b554649..e06026ce12c7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -169,8 +167,6 @@ describe('Detection rules, EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); @@ -221,8 +217,6 @@ describe('Detection rules, sequence EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 07b40df53e2d..ff000c105a1b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getIndexPatterns, getNewThreatIndicatorRule } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -482,8 +480,6 @@ describe('indicator match', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); cy.get(ALERT_RULE_SEVERITY) .first() .should('have.text', getNewThreatIndicatorRule().severity.toLowerCase()); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 24a56dd563e1..24c98aaee8f9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -16,10 +16,8 @@ import { import { NUMBER_OF_ALERTS, ALERT_RULE_NAME, - ALERT_RULE_METHOD, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, } from '../../screens/alerts'; import { @@ -196,8 +194,6 @@ describe('Detection rules, override', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat'); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical'); sortRiskScore(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index dba12fb4ab95..665df8943595 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -14,11 +14,9 @@ import { } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -179,8 +177,6 @@ describe('Detection rules, threshold', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index 270d877a362a..f762e63c899a 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,11 +1,14 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [], "include": [ - "./**/*" + "**/*", + "fixtures/**/*.json" + ], + "exclude": [ + "target/**/*" ], "compilerOptions": { - "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", + "outDir": "target/types", "types": [ "cypress", "cypress-pipe", @@ -13,4 +16,7 @@ ], "resolveJsonModule": true, }, + "references": [ + { "path": "../tsconfig.json" } + ] } diff --git a/x-pack/plugins/security_solution/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx deleted file mode 100644 index b0d2035eb446..000000000000 --- a/x-pack/plugins/security_solution/public/app/home/setup.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import { i18n } from '@kbn/i18n'; -import { NotificationsStart } from 'kibana/public'; -import { FleetStart } from '../../../../fleet/public'; - -export const Setup: React.FunctionComponent<{ - fleet: FleetStart; - notifications: NotificationsStart; -}> = ({ fleet, notifications }) => { - React.useEffect(() => { - const defaultText = i18n.translate('xpack.securitySolution.endpoint.ingestToastMessage', { - defaultMessage: 'Fleet failed during its setup.', - }); - - const title = i18n.translate('xpack.securitySolution.endpoint.ingestToastTitle', { - defaultMessage: 'App failed to initialize', - }); - - const displayToastWithModal = (text: string) => { - const errorText = new Error(defaultText); - // we're leveraging the notification's error toast which is usually used for displaying stack traces of an - // actually Error. Instead of displaying a stack trace we'll display the more detailed error text when the - // user clicks `See the full error` button to see the modal - errorText.stack = text; - notifications.toasts.addError(errorText, { - title, - }); - }; - - fleet.isInitialized().catch((error: Error) => displayToastWithModal(error.message)); - }, [fleet, notifications.toasts]); - - return null; -}; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index abffe54ab761..67d9943fabe8 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -75,6 +75,9 @@ export const SecuritySolutionTemplateWrapper: React.FC React.ReactNode; rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; + additionalFilters?: React.ReactNode; } type Props = OwnProps & PropsFromRedux; @@ -98,6 +99,7 @@ const StatefulEventsViewerComponent: React.FC = ({ showCheckboxes, sort, utilityBar, + additionalFilters, // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { @@ -165,7 +167,7 @@ const StatefulEventsViewerComponent: React.FC = ({ setGlobalFullScreen, start, sort, - utilityBar, + additionalFilters, graphEventId, filterStatus: currentFilter, leadingControlColumns, @@ -291,6 +293,7 @@ export const StatefulEventsViewer = connector( prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && + prevProps.additionalFilters === nextProps.additionalFilters && prevProps.graphEventId === nextProps.graphEventId ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index f415dc287ca3..22916b90c084 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -330,7 +330,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Details pathname', () => { const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'RULE_NAME'; + const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), @@ -357,7 +357,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Edit pathname', () => { const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'RULE_NAME'; + const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), @@ -376,7 +376,7 @@ describe('Navigation Breadcrumbs', () => { "securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { - text: 'RULE_NAME', + text: 'ALERT_RULE_NAME', href: `securitySolution/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx new file mode 100644 index 000000000000..f1d1b09f45f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx @@ -0,0 +1,65 @@ +/* + * 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, { memo } from 'react'; +import { useKibana } from '../lib/kibana'; +import { renderHook as _renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; + +jest.mock('../components/user_privileges', () => { + return { + useUserPrivileges: jest.fn().mockReturnValue({ + endpointPrivileges: { + canAccessFleet: true, + }, + }), + }; +}); +jest.mock('../lib/kibana'); + +describe('When using the `useUpgradeSecurityPackages()` hook', () => { + let renderResult: RenderHookResult; + let renderHook: () => RenderHookResult; + let kibana: ReturnType; + + // eslint-disable-next-line react/display-name + const Wrapper = memo(({ children }) => { + kibana = useKibana(); + return <>{children}; + }); + + beforeEach(() => { + renderHook = () => { + renderResult = _renderHook(() => useUpgradeSecurityPackages(), { wrapper: Wrapper }); + return renderResult; + }; + }); + + afterEach(() => { + if (renderResult) { + renderResult.unmount(); + } + }); + + it('should call fleet setup first via `isInitialized()` and then send upgrade request', async () => { + renderHook(); + + expect(kibana.services.fleet?.isInitialized).toHaveBeenCalled(); + expect(kibana.services.http.post).not.toHaveBeenCalled(); + + await renderResult.waitFor( + () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 + ); + + expect(kibana.services.http.post).toHaveBeenCalledWith( + '/api/fleet/epm/packages/_bulk', + expect.objectContaining({ + body: '{"packages":["endpoint","security_detection_engine"]}', + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts index ef1e658d349b..c09ae2715277 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts @@ -7,9 +7,8 @@ import { useEffect } from 'react'; import { HttpFetchOptions, HttpStart } from 'kibana/public'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../lib/kibana'; import { epmRouteService, BulkInstallPackagesResponse } from '../../../../fleet/common'; -import { StartServices } from '../../types'; import { useUserPrivileges } from '../components/user_privileges'; /** @@ -31,7 +30,7 @@ const sendUpgradeSecurityPackages = async ( }; export const useUpgradeSecurityPackages = () => { - const context = useKibana(); + const context = useKibana(); const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; useEffect(() => { @@ -47,20 +46,23 @@ export const useUpgradeSecurityPackages = () => { (async () => { try { + // Make sure fleet is initialized first + await context.services.fleet?.isInitialized(); + // ignore the response for now since we aren't notifying the user await sendUpgradeSecurityPackages(context.services.http, { signal }); } catch (error) { // Ignore Errors, since this should not hinder the user's ability to use the UI - // ignore the error that occurs from aborting a request + // log to console, except if the error occurred due to aborting a request if (!abortController.signal.aborted) { // eslint-disable-next-line no-console console.error(error); } } - - return abortRequests; })(); + + return abortRequests; } - }, [canAccessFleet, context.services.http]); + }, [canAccessFleet, context.services.fleet, context.services.http]); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index 13db6e94d2ee..1e286931bf79 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -7,7 +7,7 @@ import { isEmpty, isString, flow } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, Query, diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 5fd83b72af09..62d8628dc159 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -40,6 +40,7 @@ import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage import { MlLocatorDefinition } from '../../../../../ml/public'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { MockUrlService } from 'src/plugins/share/common/mocks'; +import { fleetMock } from '../../../../../fleet/public/mocks'; const mockUiSettings: Record = { [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, @@ -96,6 +97,7 @@ export const createStartServicesMock = (): StartServices => { const security = securityMock.createSetup(); const urlService = new MockUrlService(); const locator = urlService.locators.create(new MlLocatorDefinition()); + const fleet = fleetMock.createStartMock(); return ({ ...core, @@ -141,6 +143,7 @@ export const createStartServicesMock = (): StartServices => { }, security, storage, + fleet, ml: { locator, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 0d6793eb2b88..484cd6657500 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { waitFor, act } from '@testing-library/react'; import { mount } from 'enzyme'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { esQuery, Filter } from '../../../../../../../../src/plugins/data/public'; import { TestProviders } from '../../../../common/mock'; import { SecurityPageName } from '../../../../app/types'; @@ -78,6 +78,11 @@ describe('AlertsHistogramPanel', () => { updateDateRange: jest.fn(), }; + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('renders correctly', () => { const wrapper = mount( @@ -157,7 +162,7 @@ describe('AlertsHistogramPanel', () => { combinedQueries: '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}', }; - mount( + const wrapper = mount( @@ -180,6 +185,60 @@ describe('AlertsHistogramPanel', () => { ], ]); }); + wrapper.unmount(); + }); + }); + + describe('Filters', () => { + it('filters props is valid, alerts query include filter', async () => { + const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); + const statusFilter: Filter = { + meta: { + alias: null, + disabled: false, + key: 'signal.status', + negate: false, + params: { + query: 'open', + }, + type: 'phrase', + }, + query: { + term: { + 'signal.status': 'open', + }, + }, + }; + + const props = { + ...defaultProps, + query: { query: '', language: 'kql' }, + filters: [statusFilter], + }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(mockGetAlertsHistogramQuery.mock.calls[1]).toEqual([ + 'signal.rule.name', + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', + [ + { + bool: { + filter: [{ term: { 'signal.status': 'open' } }], + must: [], + must_not: [], + should: [], + }, + }, + ], + ]); + }); + wrapper.unmount(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx index db918951b855..82f58b5b1c72 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx @@ -6,7 +6,9 @@ */ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import { rgba } from 'polished'; import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import * as i18n from '../translations'; @@ -14,6 +16,17 @@ export const FILTER_OPEN: Status = 'open'; export const FILTER_CLOSED: Status = 'closed'; export const FILTER_IN_PROGRESS: Status = 'in-progress'; +const StatusFilterButton = styled(EuiFilterButton)<{ isActive: boolean }>` + background: ${({ isActive, theme }) => (isActive ? theme.eui.euiColorPrimary : '')}; +`; + +const StatusFilterGroup = styled(EuiFilterGroup)` + background: ${({ theme }) => rgba(theme.eui.euiColorPrimary, 0.2)}; + .euiButtonEmpty--ghost:enabled:focus { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + } +`; + interface Props { onFilterGroupChanged: (filterGroup: Status) => void; } @@ -37,33 +50,39 @@ const AlertsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }, [setFilterGroup, onFilterGroupChanged]); return ( - - + {i18n.OPEN_ALERTS} - + - {i18n.IN_PROGRESS_ALERTS} - + - {i18n.CLOSED_ALERTS} - - + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 1ef79a64f831..8a88c430b03e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -50,14 +50,12 @@ const UtilityBarFlexGroup = styled(EuiFlexGroup)` min-width: 175px; `; -const BuildingBlockContainer = styled(EuiFlexItem)` - background: repeating-linear-gradient( - 127deg, - rgba(245, 167, 0, 0.2), - rgba(245, 167, 0, 0.2) 1px, - rgba(245, 167, 0, 0.05) 2px, - rgba(245, 167, 0, 0.05) 10px - ); +const AdditionalFiltersItem = styled(EuiFlexItem)` + padding: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const BuildingBlockContainer = styled(AdditionalFiltersItem)` + background: ${({ theme }) => theme.eui.euiColorHighlight}; `; const AlertsUtilityBarComponent: React.FC = ({ @@ -146,39 +144,6 @@ const AlertsUtilityBarComponent: React.FC = ({ ); - const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( - - - ) => { - closePopover(); - onShowBuildingBlockAlertsChanged(e.target.checked); - }} - checked={showBuildingBlockAlerts} - color="text" - data-test-subj="showBuildingBlockAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} - /> - - - ) => { - closePopover(); - onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); - }} - checked={showOnlyThreatIndicatorAlerts} - color="text" - data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} - /> - - - ); - const handleSelectAllAlertsClick = useCallback(() => { if (!showClearSelection) { selectAll(); @@ -233,16 +198,13 @@ const AlertsUtilityBarComponent: React.FC = ({ )} - - {i18n.ADDITIONAL_FILTERS_ACTIONS} - + @@ -260,3 +222,63 @@ export const AlertsUtilityBar = React.memo( prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts && prevProps.showOnlyThreatIndicatorAlerts === nextProps.showOnlyThreatIndicatorAlerts ); + +export const AditionalFiltersAction = ({ + areEventsLoading, + onShowBuildingBlockAlertsChanged, + showBuildingBlockAlerts, + onShowOnlyThreatIndicatorAlertsChanged, + showOnlyThreatIndicatorAlerts, +}: { + areEventsLoading: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; + showBuildingBlockAlerts: boolean; + onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; + showOnlyThreatIndicatorAlerts: boolean; +}) => { + const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ) => { + closePopover(); + onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); + }} + checked={showOnlyThreatIndicatorAlerts} + color="text" + data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} + /> + + + ); + + return ( + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 7ff6f82d40bd..0519e3f2d4a7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -8,10 +8,14 @@ import { ALERT_DURATION, ALERT_ID, - ALERT_PRODUCER, + ALERT_RULE_PRODUCER, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_ID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, } from '@kbn/rule-data-utils'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; @@ -185,11 +189,11 @@ export const requiredFieldMappingsForActionsRuleRegistry = { 'event.action': 'event.action', 'alert.status': ALERT_STATUS, 'alert.duration.us': ALERT_DURATION, - 'rule.uuid': 'rule.uuid', - 'rule.id': 'rule.id', - 'rule.name': 'rule.name', - 'rule.category': 'rule.category', - producer: ALERT_PRODUCER, + 'rule.uuid': ALERT_RULE_UUID, + 'rule.id': ALERT_RULE_ID, + 'rule.name': ALERT_RULE_NAME, + 'rule.category': ALERT_RULE_CATEGORY, + producer: ALERT_RULE_PRODUCER, tags: 'tags', }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index be11aecfe47d..dba7915460ad 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -35,9 +35,7 @@ describe('AlertsTableComponent', () => { isSelectAllChecked={false} clearSelected={jest.fn()} setEventsLoading={jest.fn()} - clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} - clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} showOnlyThreatIndicatorAlerts={false} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index a1f2025c6c0d..d8d6424ef2a7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -31,8 +31,7 @@ import { alertsDefaultModelRuleRegistry, buildAlertStatusFilterRuleRegistry, } from './default_config'; -import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; -import { AlertsUtilityBar } from './alerts_utility_bar'; +import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar'; import * as i18nCommon from '../../../common/translations'; import * as i18n from './translations'; import { @@ -68,13 +67,12 @@ interface OwnProps { showOnlyThreatIndicatorAlerts: boolean; timelineId: TimelineIdLiteral; to: string; + filterGroup?: Status; } type AlertsTableComponentProps = OwnProps & PropsFromRedux; export const AlertsTableComponent: React.FC = ({ - clearEventsDeleted, - clearEventsLoading, clearSelected, defaultFilters, from, @@ -95,10 +93,10 @@ export const AlertsTableComponent: React.FC = ({ showOnlyThreatIndicatorAlerts, timelineId, to, + filterGroup = 'open', }) => { const dispatch = useDispatch(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const { browserFields, indexPattern: indexPatterns, @@ -216,17 +214,6 @@ export const AlertsTableComponent: React.FC = ({ } }, [dispatch, isSelectAllChecked, timelineId]); - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: Status) => { - clearEventsLoading!({ id: timelineId }); - clearEventsDeleted!({ id: timelineId }); - clearSelected!({ id: timelineId }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup, timelineId] - ); - // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { clearSelected!({ id: timelineId }); @@ -326,6 +313,16 @@ export const AlertsTableComponent: React.FC = ({ ] ); + const additionalFiltersComponent = ( + 0} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged} + showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} + /> + ); + const defaultFiltersMemo = useMemo(() => { // TODO: Once we are past experimental phase this code should be removed const alertStatusFilter = ruleRegistryEnabled @@ -372,11 +369,6 @@ export const AlertsTableComponent: React.FC = ({ ); }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); - const headerFilterGroup = useMemo( - () => , - [onFilterGroupChangedCallback] - ); - if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return ( @@ -393,7 +385,6 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={defaultTimelineModel} end={to} currentFilter={filterGroup} - headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} renderCellValue={RenderCellValue} @@ -401,6 +392,7 @@ export const AlertsTableComponent: React.FC = ({ scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} + additionalFilters={additionalFiltersComponent} /> ); }; @@ -438,8 +430,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ eventIds: string[]; isLoading: boolean; }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsLoading({ id })), setEventsDeleted: ({ id, eventIds, @@ -449,8 +439,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ eventIds: string[]; isDeleted: boolean; }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsDeleted({ id })), }); const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx index 23709269a4c1..7be51c4eaa41 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -27,7 +27,7 @@ const AddEndpointExceptionComponent: React.FC = ({ onClick={onClick} disabled={disabled} > - {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx index 1104b3eb8308..9b14c01371c9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -24,9 +24,7 @@ const AddEventFilterComponent: React.FC = ({ onClick, disab onClick={onClick} disabled={disabled} > - - {i18n.ACTION_ADD_EVENT_FILTER} - + {i18n.ACTION_ADD_EVENT_FILTER} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx index 030f67c9e708..99eef3aefd42 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -24,9 +24,7 @@ const AddExceptionComponent: React.FC = ({ disabled, onClick onClick={onClick} disabled={disabled} > - - {i18n.ACTION_ADD_EXCEPTION} - + {i18n.ACTION_ADD_EXCEPTION} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 3a9a4e875369..2dae69fec43e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -7,8 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { indexOf } from 'lodash'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; @@ -31,8 +30,10 @@ import { useAlertsActions } from './use_alerts_actions'; import { useExceptionModal } from './use_add_exception_modal'; import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; -import { useEventFilterAction } from './use_event_filter_action'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { AddEventFilter } from './add_event_filter'; +import { AddException } from './add_exception'; +import { AddEndpointException } from './add_endpoint_exception'; interface AlertContextMenuProps { ariaLabel?: string; @@ -112,7 +113,7 @@ const AlertContextMenuComponent: React.FC = ({ onAddEventFilterClick, } = useEventFilterModal(); - const { statusActions } = useAlertsActions({ + const { actionItems } = useAlertsActions({ alertStatus, eventId: ecsRowData?._id, timelineId, @@ -132,23 +133,41 @@ const AlertContextMenuComponent: React.FC = ({ closePopover(); }, [closePopover, onAddEventFilterClick]); - const exceptionActions = useExceptionActions({ + const { + disabledAddEndpointException, + disabledAddException, + handleEndpointExceptionModal, + handleDetectionExceptionModal, + } = useExceptionActions({ isEndpointAlert, onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); - const eventFilterActions = useEventFilterAction({ - onAddEventFilterClick: handleOnAddEventFilterClick, - }); - - const panels = useMemo( - () => [ - { - id: 0, - items: !isEvent && ruleId ? [...statusActions, ...exceptionActions] : [eventFilterActions], - }, - ], - [eventFilterActions, exceptionActions, isEvent, ruleId, statusActions] + const items = useMemo( + () => + !isEvent && ruleId + ? [ + ...actionItems, + , + , + ] + : [], + [ + actionItems, + disabledAddEndpointException, + disabledAddException, + handleDetectionExceptionModal, + handleEndpointExceptionModal, + handleOnAddEventFilterClick, + isEvent, + ruleId, + ] ); return ( @@ -164,7 +183,7 @@ const AlertContextMenuComponent: React.FC = ({ anchorPosition="downLeft" repositionOnScroll > - + @@ -191,12 +210,6 @@ const AlertContextMenuComponent: React.FC = ({ ); }; -const ContextMenuPanel = styled(EuiContextMenu)` - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; -`; - -ContextMenuPanel.displayName = 'ContextMenuPanel'; - export const AlertContextMenu = React.memo(AlertContextMenuComponent); type AddExceptionModalWrapperProps = Omit< diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx index 038d58c38a01..28a34c549ef1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_CLOSED } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -25,7 +25,7 @@ const CloseAlertActionComponent: React.FC = ({ onClick, d onClick={onClick} disabled={disabled} > - {i18n.ACTION_CLOSE_ALERT} + {i18n.ACTION_CLOSE_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx index 2bca56903282..f273833c1c1b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_IN_PROGRESS } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -28,7 +28,7 @@ const InProgressAlertStatusComponent: React.FC = ({ onClick={onClick} disabled={disabled} > - {i18n.ACTION_IN_PROGRESS_ALERT} + {i18n.ACTION_IN_PROGRESS_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx index 34832ee07ea7..2042acea4d60 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_OPEN } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -25,7 +25,7 @@ const OpenAlertStatusComponent: React.FC = ({ onClick, dis onClick={onClick} disabled={disabled} > - {i18n.ACTION_OPEN_ALERT} + {i18n.ACTION_OPEN_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx index 0f8fa00a3ac4..9f1f699241e2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx @@ -11,12 +11,20 @@ import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { useUserData } from '../../user_info'; import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations'; -interface UseExceptionActions { +interface ExceptionActions { name: string; onClick: () => void; disabled: boolean; } +interface UseExceptionActions { + disabledAddEndpointException: boolean; + disabledAddException: boolean; + exceptionActions: ExceptionActions[]; + handleEndpointExceptionModal: () => void; + handleDetectionExceptionModal: () => void; +} + interface UseExceptionActionProps { isEndpointAlert: boolean; onAddExceptionTypeClick: (type: ExceptionListType) => void; @@ -25,7 +33,7 @@ interface UseExceptionActionProps { export const useExceptionActions = ({ isEndpointAlert, onAddExceptionTypeClick, -}: UseExceptionActionProps): UseExceptionActions[] => { +}: UseExceptionActionProps): UseExceptionActions => { const [{ canUserCRUD, hasIndexWrite }] = useUserData(); const handleDetectionExceptionModal = useCallback(() => { @@ -62,5 +70,11 @@ export const useExceptionActions = ({ ] ); - return exceptionActions; + return { + disabledAddEndpointException, + disabledAddException, + exceptionActions, + handleEndpointExceptionModal, + handleDetectionExceptionModal, + }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 855eb2dd5fef..4fdebee6e1f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; -import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group'; -import { updateAlertStatusAction } from '../actions'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; import * as i18nCommon from '../../../../common/translations'; import * as i18n from '../translations'; @@ -22,12 +20,12 @@ import { displaySuccessToast, displayErrorToast, } from '../../../../common/components/toasters'; -import { useUserData } from '../../user_info'; +import { useStatusBulkActionItems } from '../../../../../../timelines/public'; interface Props { - alertStatus?: string; + alertStatus?: Status; closePopover: () => void; - eventId: string | null | undefined; + eventId: string; timelineId: string; } @@ -37,10 +35,9 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI const { addWarning } = useAppToasts(); - const [{ canUserCRUD, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData(); - const onAlertStatusUpdateSuccess = useCallback( (updated: number, conflicts: number, newStatus: Status) => { + closePopover(); if (conflicts > 0) { // Partial failure addWarning({ @@ -63,12 +60,14 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI displaySuccessToast(title, dispatchToaster); } }, - [addWarning, dispatchToaster] + [addWarning, closePopover, dispatchToaster] ); const onAlertStatusUpdateFailure = useCallback( (newStatus: Status, error: Error) => { let title: string; + closePopover(); + switch (newStatus) { case 'closed': title = i18n.CLOSED_ALERT_FAILED_TOAST; @@ -81,7 +80,7 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI } displayErrorToast(title, [error.message], dispatchToaster); }, - [dispatchToaster] + [closePopover, dispatchToaster] ); const setEventsLoading = useCallback( @@ -98,120 +97,16 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI [dispatch, timelineId] ); - const openAlertActionOnClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_OPEN, - }); - } - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - ]); - - const closeAlertActionClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_CLOSED, - }); - } - - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, + const actionItems = useStatusBulkActionItems({ + eventIds: [eventId], + currentStatus: alertStatus, setEventsLoading, - ]); - - const inProgressAlertActionClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_IN_PROGRESS, - }); - } - - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, setEventsDeleted, - setEventsLoading, - ]); - - const disabledInProgressAlertAction = !canUserCRUD || !hasIndexUpdateDelete; - - const inProgressAlertAction = useMemo(() => { - return { - name: i18n.ACTION_IN_PROGRESS_ALERT, - disabled: disabledInProgressAlertAction, - onClick: inProgressAlertActionClick, - [`data-test-subj`]: 'in-progress-alert-status', - }; - }, [disabledInProgressAlertAction, inProgressAlertActionClick]); - - const disabledCloseAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance; - const closeAlertAction = useMemo(() => { - return { - name: i18n.ACTION_CLOSE_ALERT, - disabled: disabledCloseAlertAction, - onClick: closeAlertActionClick, - [`data-test-subj`]: 'close-alert-status', - }; - }, [disabledCloseAlertAction, closeAlertActionClick]); - - const disabledOpenAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance; - const openAlertAction = useMemo(() => { - return { - name: i18n.ACTION_OPEN_ALERT, - disabled: disabledOpenAlertAction, - onClick: openAlertActionOnClick, - [`data-test-subj`]: 'open-alert-status', - }; - }, [disabledOpenAlertAction, openAlertActionOnClick]); - - const statusActions = useMemo(() => { - if (!alertStatus) { - return []; - } - - switch (alertStatus) { - case 'open': - return [inProgressAlertAction, closeAlertAction]; - case 'in-progress': - return [openAlertAction, closeAlertAction]; - case 'closed': - return [openAlertAction, inProgressAlertAction]; - default: - return []; - } - }, [alertStatus, inProgressAlertAction, closeAlertAction, openAlertAction]); + onUpdateSuccess: onAlertStatusUpdateSuccess, + onUpdateFailure: onAlertStatusUpdateFailure, + }); return { - statusActions, + actionItems, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index d0f26894bf7d..ffaea216f3fe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { EuiContextMenu, EuiButton, EuiPopover } from '@elastic/eui'; +import { EuiContextMenu, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; @@ -135,7 +135,7 @@ export const TakeActionDropdown = React.memo( [onAddExceptionTypeClick] ); - const exceptionActions = useExceptionActions({ + const { exceptionActions } = useExceptionActions({ isEndpointAlert, onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); @@ -149,7 +149,7 @@ export const TakeActionDropdown = React.memo( onAddEventFilterClick: handleOnAddEventFilterClick, }); - const { statusActions } = useAlertsActions({ + const { actionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, eventId: actionsData.eventId, timelineId, @@ -191,7 +191,7 @@ export const TakeActionDropdown = React.memo( { id: 1, title: CHANGE_ALERT_STATUS, - items: statusActions, + content: , }, /* Todo: Uncomment case action after getAddToCaseAction is split into action and modal { @@ -210,7 +210,7 @@ export const TakeActionDropdown = React.memo( ), },*/ ], - [alertsActionItems, hostIsolationAction, investigateInTimelineAction, statusActions] + [actionItems, alertsActionItems, hostIsolationAction, investigateInTimelineAction] ); const takeActionButton = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index d6d3d829d3be..89de83ab6e5c 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -35,18 +35,6 @@ export const columns: Array< initialWidth: DEFAULT_COLUMN_MIN_WIDTH, linkField: 'signal.rule.id', }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_VERSION, - id: 'signal.rule.version', - initialWidth: 95, - }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_METHOD, - id: 'signal.rule.type', - initialWidth: 100, - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -57,31 +45,29 @@ export const columns: Array< columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_RISK_SCORE, id: 'signal.rule.risk_score', - initialWidth: 115, + initialWidth: 100, }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.module', - linkField: 'rule.reference', + displayAsText: i18n.ALERTS_HEADERS_REASON, + id: 'signal.reason', + initialWidth: 450, }, { - aggregatable: true, - category: 'event', columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - type: 'string', + id: 'host.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.category', + id: 'user.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'host.name', + id: 'process.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'user.name', + id: 'file.name', }, { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts index 8af478128492..b082d90fc148 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import { useEffect, useState } from 'react'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { isEmpty } from 'lodash'; import { @@ -37,7 +37,7 @@ export const useFetchEcsAlertsData = ({ try { setIsLoading(true); const alertResponse = await KibanaServices.get().http.fetch< - SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', body: JSON.stringify(buildAlertsQuery(alertIds ?? [])), @@ -45,10 +45,10 @@ export const useFetchEcsAlertsData = ({ setAlertEcsData( alertResponse?.hits.hits.reduce( - (acc, { _id, _index, _source }) => [ + (acc, { _id, _index, _source = {} }) => [ ...acc, { - ...formatAlertToEcsSignal(_source as {}), + ...formatAlertToEcsSignal(_source), _id, _index, timestamp: _source['@timestamp'], diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index f28311d9c96e..3009ad8cdd01 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -340,6 +340,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1110', tactics: ['credential-access'], }, + { + name: 'Build Image on Host', + id: 'T1612', + reference: 'https://attack.mitre.org/techniques/T1612', + tactics: ['defense-evasion'], + }, { name: 'Clipboard Data', id: 'T1115', @@ -406,6 +412,18 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1584', tactics: ['resource-development'], }, + { + name: 'Container Administration Command', + id: 'T1609', + reference: 'https://attack.mitre.org/techniques/T1609', + tactics: ['execution'], + }, + { + name: 'Container and Resource Discovery', + id: 'T1613', + reference: 'https://attack.mitre.org/techniques/T1613', + tactics: ['discovery'], + }, { name: 'Create Account', id: 'T1136', @@ -514,6 +532,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1140', tactics: ['defense-evasion'], }, + { + name: 'Deploy Container', + id: 'T1610', + reference: 'https://attack.mitre.org/techniques/T1610', + tactics: ['defense-evasion', 'execution'], + }, { name: 'Develop Capabilities', id: 'T1587', @@ -532,6 +556,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1561', tactics: ['impact'], }, + { + name: 'Domain Policy Modification', + id: 'T1484', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: ['defense-evasion', 'privilege-escalation'], + }, { name: 'Domain Trust Discovery', id: 'T1482', @@ -568,6 +598,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1499', tactics: ['impact'], }, + { + name: 'Escape to Host', + id: 'T1611', + reference: 'https://attack.mitre.org/techniques/T1611', + tactics: ['privilege-escalation'], + }, { name: 'Establish Accounts', id: 'T1585', @@ -688,6 +724,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1187', tactics: ['credential-access'], }, + { + name: 'Forge Web Credentials', + id: 'T1606', + reference: 'https://attack.mitre.org/techniques/T1606', + tactics: ['credential-access'], + }, { name: 'Gather Victim Host Information', id: 'T1592', @@ -749,7 +791,7 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Implant Container Image', + name: 'Implant Internal Image', id: 'T1525', reference: 'https://attack.mitre.org/techniques/T1525', tactics: ['persistence'], @@ -830,7 +872,7 @@ export const technique = [ name: 'Modify Authentication Process', id: 'T1556', reference: 'https://attack.mitre.org/techniques/T1556', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], }, { name: 'Modify Cloud Compute Infrastructure', @@ -1162,6 +1204,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1153', tactics: ['execution'], }, + { + name: 'Stage Capabilities', + id: 'T1608', + reference: 'https://attack.mitre.org/techniques/T1608', + tactics: ['resource-development'], + }, { name: 'Steal Application Access Token', id: 'T1528', @@ -1198,6 +1246,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1082', tactics: ['discovery'], }, + { + name: 'System Location Discovery', + id: 'T1614', + reference: 'https://attack.mitre.org/techniques/T1614', + tactics: ['discovery'], + }, { name: 'System Network Configuration Discovery', id: 'T1016', @@ -1348,18 +1402,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1220', tactics: ['defense-evasion'], }, - { - name: 'Domain Policy Modification', - id: 'T1484', - reference: 'https://attack.mitre.org/techniques/T1484', - tactics: ['defense-evasion', 'privilege-escalation'], - }, - { - name: 'Forge Web Credentials', - id: 'T1606', - reference: 'https://attack.mitre.org/techniques/T1606', - tactics: ['credential-access'], - }, ]; export const techniquesOptions: MitreTechniquesOptions[] = [ @@ -1572,6 +1614,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'credential-access', value: 'bruteForce', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.buildImageOnHostDescription', + { defaultMessage: 'Build Image on Host (T1612)' } + ), + id: 'T1612', + name: 'Build Image on Host', + reference: 'https://attack.mitre.org/techniques/T1612', + tactics: 'defense-evasion', + value: 'buildImageOnHost', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription', @@ -1693,6 +1746,28 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'resource-development', value: 'compromiseInfrastructure', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.containerAdministrationCommandDescription', + { defaultMessage: 'Container Administration Command (T1609)' } + ), + id: 'T1609', + name: 'Container Administration Command', + reference: 'https://attack.mitre.org/techniques/T1609', + tactics: 'execution', + value: 'containerAdministrationCommand', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.containerAndResourceDiscoveryDescription', + { defaultMessage: 'Container and Resource Discovery (T1613)' } + ), + id: 'T1613', + name: 'Container and Resource Discovery', + reference: 'https://attack.mitre.org/techniques/T1613', + tactics: 'discovery', + value: 'containerAndResourceDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription', @@ -1891,6 +1966,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'deobfuscateDecodeFilesOrInformation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.deployContainerDescription', + { defaultMessage: 'Deploy Container (T1610)' } + ), + id: 'T1610', + name: 'Deploy Container', + reference: 'https://attack.mitre.org/techniques/T1610', + tactics: 'defense-evasion,execution', + value: 'deployContainer', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.developCapabilitiesDescription', @@ -1924,6 +2010,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'impact', value: 'diskWipe', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainPolicyModificationDescription', + { defaultMessage: 'Domain Policy Modification (T1484)' } + ), + id: 'T1484', + name: 'Domain Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: 'defense-evasion,privilege-escalation', + value: 'domainPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription', @@ -1990,6 +2087,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'impact', value: 'endpointDenialOfService', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.escapeToHostDescription', + { defaultMessage: 'Escape to Host (T1611)' } + ), + id: 'T1611', + name: 'Escape to Host', + reference: 'https://attack.mitre.org/techniques/T1611', + tactics: 'privilege-escalation', + value: 'escapeToHost', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.establishAccountsDescription', @@ -2210,6 +2318,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'credential-access', value: 'forcedAuthentication', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.forgeWebCredentialsDescription', + { defaultMessage: 'Forge Web Credentials (T1606)' } + ), + id: 'T1606', + name: 'Forge Web Credentials', + reference: 'https://attack.mitre.org/techniques/T1606', + tactics: 'credential-access', + value: 'forgeWebCredentials', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimHostInformationDescription', @@ -2322,14 +2441,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription', - { defaultMessage: 'Implant Container Image (T1525)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantInternalImageDescription', + { defaultMessage: 'Implant Internal Image (T1525)' } ), id: 'T1525', - name: 'Implant Container Image', + name: 'Implant Internal Image', reference: 'https://attack.mitre.org/techniques/T1525', tactics: 'persistence', - value: 'implantContainerImage', + value: 'implantInternalImage', }, { label: i18n.translate( @@ -2471,7 +2590,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1556', name: 'Modify Authentication Process', reference: 'https://attack.mitre.org/techniques/T1556', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', value: 'modifyAuthenticationProcess', }, { @@ -3079,6 +3198,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'execution', value: 'source', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stageCapabilitiesDescription', + { defaultMessage: 'Stage Capabilities (T1608)' } + ), + id: 'T1608', + name: 'Stage Capabilities', + reference: 'https://attack.mitre.org/techniques/T1608', + tactics: 'resource-development', + value: 'stageCapabilities', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', @@ -3145,6 +3275,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'systemInformationDiscovery', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemLocationDiscoveryDescription', + { defaultMessage: 'System Location Discovery (T1614)' } + ), + id: 'T1614', + name: 'System Location Discovery', + reference: 'https://attack.mitre.org/techniques/T1614', + tactics: 'discovery', + value: 'systemLocationDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription', @@ -3420,38 +3561,9 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'xslScriptProcessing', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainPolicyModificationDescription', - { defaultMessage: 'Domain Policy Modification (T1484)' } - ), - id: 'T1484', - name: 'Domain Policy Modification', - reference: 'https://attack.mitre.org/techniques/T1484', - tactics: 'defense-evasion,privilege-escalation', - value: 'domainPolicyModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.forgeWebCredentialsDescription', - { defaultMessage: 'Forge Web Credentials (T1606)' } - ), - id: 'T1606', - name: 'Forge Web Credentials', - reference: 'https://attack.mitre.org/techniques/T1606', - tactics: 'credential-access', - value: 'forgeWebCredentials', - }, ]; export const subtechniques = [ - { - name: '.bash_profile and .bashrc', - id: 'T1546.004', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: ['privilege-escalation', 'persistence'], - techniqueId: 'T1546', - }, { name: '/etc/passwd and /etc/shadow', id: 'T1003.008', @@ -3480,6 +3592,13 @@ export const subtechniques = [ tactics: ['privilege-escalation', 'persistence'], techniqueId: 'T1546', }, + { + name: 'Active Setup', + id: 'T1547.014', + reference: 'https://attack.mitre.org/techniques/T1547/014', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, { name: 'Add Office 365 Global Administrator Role', id: 'T1098.003', @@ -3494,6 +3613,13 @@ export const subtechniques = [ tactics: ['persistence'], techniqueId: 'T1137', }, + { + name: 'Additional Cloud Credentials', + id: 'T1098.001', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: ['persistence'], + techniqueId: 'T1098', + }, { name: 'AppCert DLLs', id: 'T1546.009', @@ -3774,6 +3900,13 @@ export const subtechniques = [ tactics: ['resource-development'], techniqueId: 'T1588', }, + { + name: 'Code Signing Policy Modification', + id: 'T1553.006', + reference: 'https://attack.mitre.org/techniques/T1553/006', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, { name: 'Compile After Delivery', id: 'T1027.004', @@ -3837,6 +3970,20 @@ export const subtechniques = [ tactics: ['collection'], techniqueId: 'T1213', }, + { + name: 'Container API', + id: 'T1552.007', + reference: 'https://attack.mitre.org/techniques/T1552/007', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Container Orchestration Job', + id: 'T1053.007', + reference: 'https://attack.mitre.org/techniques/T1053/007', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, { name: 'Control Panel', id: 'T1218.002', @@ -4121,7 +4268,7 @@ export const subtechniques = [ name: 'Domain Controller Authentication', id: 'T1556.001', reference: 'https://attack.mitre.org/techniques/T1556/001', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4152,6 +4299,13 @@ export const subtechniques = [ tactics: ['reconnaissance'], techniqueId: 'T1590', }, + { + name: 'Domain Trust Modification', + id: 'T1484.002', + reference: 'https://attack.mitre.org/techniques/T1484/002', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1484', + }, { name: 'Domains', id: 'T1583.001', @@ -4173,6 +4327,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1601', }, + { + name: 'Drive-by Target', + id: 'T1608.004', + reference: 'https://attack.mitre.org/techniques/T1608/004', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Dylib Hijacking', id: 'T1574.004', @@ -4187,6 +4348,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1559', }, + { + name: 'Dynamic Linker Hijacking', + id: 'T1574.006', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, { name: 'Dynamic-link Library Injection', id: 'T1055.001', @@ -4404,6 +4572,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1558', }, + { + name: 'Group Policy Modification', + id: 'T1484.001', + reference: 'https://attack.mitre.org/techniques/T1484/001', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1484', + }, { name: 'Group Policy Preferences', id: 'T1552.006', @@ -4495,6 +4670,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1027', }, + { + name: 'Install Digital Certificate', + id: 'T1608.003', + reference: 'https://attack.mitre.org/techniques/T1608/003', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Install Root Certificate', id: 'T1553.004', @@ -4523,6 +4705,13 @@ export const subtechniques = [ tactics: ['command-and-control'], techniqueId: 'T1090', }, + { + name: 'Internet Connection Discovery', + id: 'T1016.001', + reference: 'https://attack.mitre.org/techniques/T1016/001', + tactics: ['discovery'], + techniqueId: 'T1016', + }, { name: 'Invalid Code Signature', id: 'T1036.001', @@ -4531,7 +4720,7 @@ export const subtechniques = [ techniqueId: 'T1036', }, { - name: 'JavaScript/JScript', + name: 'JavaScript', id: 'T1059.007', reference: 'https://attack.mitre.org/techniques/T1059/007', tactics: ['execution'], @@ -4579,13 +4768,6 @@ export const subtechniques = [ tactics: ['privilege-escalation', 'persistence'], techniqueId: 'T1546', }, - { - name: 'LD_PRELOAD', - id: 'T1574.006', - reference: 'https://attack.mitre.org/techniques/T1574/006', - tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], - techniqueId: 'T1574', - }, { name: 'LLMNR/NBT-NS Poisoning and SMB Relay', id: 'T1557.001', @@ -4642,6 +4824,13 @@ export const subtechniques = [ tactics: ['execution', 'persistence', 'privilege-escalation'], techniqueId: 'T1053', }, + { + name: 'Link Target', + id: 'T1608.005', + reference: 'https://attack.mitre.org/techniques/T1608/005', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Linux and Mac File and Directory Permissions Modification', id: 'T1222.002', @@ -4733,6 +4922,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1204', }, + { + name: 'Malicious Image', + id: 'T1204.003', + reference: 'https://attack.mitre.org/techniques/T1204/003', + tactics: ['execution'], + techniqueId: 'T1204', + }, { name: 'Malicious Link', id: 'T1204.001', @@ -4754,6 +4950,13 @@ export const subtechniques = [ tactics: ['resource-development'], techniqueId: 'T1588', }, + { + name: 'Mark-of-the-Web Bypass', + id: 'T1553.005', + reference: 'https://attack.mitre.org/techniques/T1553/005', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, { name: 'Masquerade Task or Service', id: 'T1036.004', @@ -4821,7 +5024,7 @@ export const subtechniques = [ name: 'Network Device Authentication', id: 'T1556.004', reference: 'https://attack.mitre.org/techniques/T1556/004', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4968,7 +5171,7 @@ export const subtechniques = [ name: 'Password Filter DLL', id: 'T1556.002', reference: 'https://attack.mitre.org/techniques/T1556/002', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4978,6 +5181,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1110', }, + { + name: 'Password Managers', + id: 'T1555.005', + reference: 'https://attack.mitre.org/techniques/T1555/005', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, { name: 'Password Spraying', id: 'T1110.003', @@ -5024,7 +5234,7 @@ export const subtechniques = [ name: 'Pluggable Authentication Modules', id: 'T1556.003', reference: 'https://attack.mitre.org/techniques/T1556/003', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -5139,6 +5349,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'RC Scripts', + id: 'T1037.004', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, { name: 'RDP Hijacking', id: 'T1563.002', @@ -5153,13 +5370,6 @@ export const subtechniques = [ tactics: ['defense-evasion', 'persistence'], techniqueId: 'T1542', }, - { - name: 'Rc.common', - id: 'T1037.004', - reference: 'https://attack.mitre.org/techniques/T1037/004', - tactics: ['persistence', 'privilege-escalation'], - techniqueId: 'T1037', - }, { name: 'Re-opened Applications', id: 'T1547.007', @@ -5265,6 +5475,13 @@ export const subtechniques = [ tactics: ['impact'], techniqueId: 'T1565', }, + { + name: 'SAML Tokens', + id: 'T1606.002', + reference: 'https://attack.mitre.org/techniques/T1606/002', + tactics: ['credential-access'], + techniqueId: 'T1606', + }, { name: 'SID-History Injection', id: 'T1134.005', @@ -5713,6 +5930,27 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'Unix Shell Configuration Modification', + id: 'T1546.004', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Upload Malware', + id: 'T1608.001', + reference: 'https://attack.mitre.org/techniques/T1608/001', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, + { + name: 'Upload Tool', + id: 'T1608.002', + reference: 'https://attack.mitre.org/techniques/T1608/002', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'User Activity Based Checks', id: 'T1497.002', @@ -5790,6 +6028,13 @@ export const subtechniques = [ tactics: ['reconnaissance'], techniqueId: 'T1596', }, + { + name: 'Web Cookies', + id: 'T1606.001', + reference: 'https://attack.mitre.org/techniques/T1606/001', + tactics: ['credential-access'], + techniqueId: 'T1606', + }, { name: 'Web Portal Capture', id: 'T1056.003', @@ -5839,6 +6084,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'Windows Credential Manager', + id: 'T1555.004', + reference: 'https://attack.mitre.org/techniques/T1555/004', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, { name: 'Windows File and Directory Permissions Modification', id: 'T1222.001', @@ -5875,55 +6127,15 @@ export const subtechniques = [ techniqueId: 'T1547', }, { - name: 'Additional Cloud Credentials', - id: 'T1098.001', - reference: 'https://attack.mitre.org/techniques/T1098/001', - tactics: ['persistence'], - techniqueId: 'T1098', - }, - { - name: 'Group Policy Modification', - id: 'T1484.001', - reference: 'https://attack.mitre.org/techniques/T1484/001', - tactics: ['defense-evasion', 'privilege-escalation'], - techniqueId: 'T1484', - }, - { - name: 'Domain Trust Modification', - id: 'T1484.002', - reference: 'https://attack.mitre.org/techniques/T1484/002', - tactics: ['defense-evasion', 'privilege-escalation'], - techniqueId: 'T1484', - }, - { - name: 'Web Cookies', - id: 'T1606.001', - reference: 'https://attack.mitre.org/techniques/T1606/001', - tactics: ['credential-access'], - techniqueId: 'T1606', - }, - { - name: 'SAML Tokens', - id: 'T1606.002', - reference: 'https://attack.mitre.org/techniques/T1606/002', - tactics: ['credential-access'], - techniqueId: 'T1606', - }, -]; - -export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description', - { defaultMessage: '.bash_profile and .bashrc (T1546.004)' } - ), - id: 'T1546.004', - name: '.bash_profile and .bashrc', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: 'privilege-escalation,persistence', - techniqueId: 'T1546', - value: 'bashProfileAndBashrc', + name: 'XDG Autostart Entries', + id: 'T1547.013', + reference: 'https://attack.mitre.org/techniques/T1547/013', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', }, +]; + +export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.etcPasswdAndEtcShadowT1003Description', @@ -5972,6 +6184,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1546', value: 'accessibilityFeatures', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.activeSetupT1547Description', + { defaultMessage: 'Active Setup (T1547.014)' } + ), + id: 'T1547.014', + name: 'Active Setup', + reference: 'https://attack.mitre.org/techniques/T1547/014', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'activeSetup', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.addOffice365GlobalAdministratorRoleT1098Description', @@ -5996,6 +6220,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1137', value: 'addIns', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.additionalCloudCredentialsT1098Description', + { defaultMessage: 'Additional Cloud Credentials (T1098.001)' } + ), + id: 'T1098.001', + name: 'Additional Cloud Credentials', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'additionalCloudCredentials', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.appCertDlLsT1546Description', @@ -6476,6 +6712,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1588', value: 'codeSigningCertificates', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningPolicyModificationT1553Description', + { defaultMessage: 'Code Signing Policy Modification (T1553.006)' } + ), + id: 'T1553.006', + name: 'Code Signing Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1553/006', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'codeSigningPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compileAfterDeliveryT1027Description', @@ -6584,6 +6832,30 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1213', value: 'confluence', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.containerApiT1552Description', + { defaultMessage: 'Container API (T1552.007)' } + ), + id: 'T1552.007', + name: 'Container API', + reference: 'https://attack.mitre.org/techniques/T1552/007', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'containerApi', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.containerOrchestrationJobT1053Description', + { defaultMessage: 'Container Orchestration Job (T1053.007)' } + ), + id: 'T1053.007', + name: 'Container Orchestration Job', + reference: 'https://attack.mitre.org/techniques/T1053/007', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'containerOrchestrationJob', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.controlPanelT1218Description', @@ -7072,7 +7344,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.001', name: 'Domain Controller Authentication', reference: 'https://attack.mitre.org/techniques/T1556/001', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'domainControllerAuthentication', }, @@ -7124,6 +7396,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1590', value: 'domainProperties', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainTrustModificationT1484Description', + { defaultMessage: 'Domain Trust Modification (T1484.002)' } + ), + id: 'T1484.002', + name: 'Domain Trust Modification', + reference: 'https://attack.mitre.org/techniques/T1484/002', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1484', + value: 'domainTrustModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainsT1583Description', @@ -7160,6 +7444,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1601', value: 'downgradeSystemImage', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.driveByTargetT1608Description', + { defaultMessage: 'Drive-by Target (T1608.004)' } + ), + id: 'T1608.004', + name: 'Drive-by Target', + reference: 'https://attack.mitre.org/techniques/T1608/004', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'driveByTarget', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dylibHijackingT1574Description', @@ -7184,6 +7480,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1559', value: 'dynamicDataExchange', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicLinkerHijackingT1574Description', + { defaultMessage: 'Dynamic Linker Hijacking (T1574.006)' } + ), + id: 'T1574.006', + name: 'Dynamic Linker Hijacking', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'dynamicLinkerHijacking', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicLinkLibraryInjectionT1055Description', @@ -7556,6 +7864,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1558', value: 'goldenTicket', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyModificationT1484Description', + { defaultMessage: 'Group Policy Modification (T1484.001)' } + ), + id: 'T1484.001', + name: 'Group Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1484/001', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1484', + value: 'groupPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyPreferencesT1552Description', @@ -7712,6 +8032,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1027', value: 'indicatorRemovalFromTools', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installDigitalCertificateT1608Description', + { defaultMessage: 'Install Digital Certificate (T1608.003)' } + ), + id: 'T1608.003', + name: 'Install Digital Certificate', + reference: 'https://attack.mitre.org/techniques/T1608/003', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'installDigitalCertificate', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installRootCertificateT1553Description', @@ -7760,6 +8092,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1090', value: 'internalProxy', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internetConnectionDiscoveryT1016Description', + { defaultMessage: 'Internet Connection Discovery (T1016.001)' } + ), + id: 'T1016.001', + name: 'Internet Connection Discovery', + reference: 'https://attack.mitre.org/techniques/T1016/001', + tactics: 'discovery', + techniqueId: 'T1016', + value: 'internetConnectionDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description', @@ -7774,15 +8118,15 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description', - { defaultMessage: 'JavaScript/JScript (T1059.007)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptT1059Description', + { defaultMessage: 'JavaScript (T1059.007)' } ), id: 'T1059.007', - name: 'JavaScript/JScript', + name: 'JavaScript', reference: 'https://attack.mitre.org/techniques/T1059/007', tactics: 'execution', techniqueId: 'T1059', - value: 'javaScriptJScript', + value: 'javaScript', }, { label: i18n.translate( @@ -7856,18 +8200,6 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1546', value: 'lcLoadDylibAddition', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description', - { defaultMessage: 'LD_PRELOAD (T1574.006)' } - ), - id: 'T1574.006', - name: 'LD_PRELOAD', - reference: 'https://attack.mitre.org/techniques/T1574/006', - tactics: 'persistence,privilege-escalation,defense-evasion', - techniqueId: 'T1574', - value: 'ldPreload', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description', @@ -7964,6 +8296,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1053', value: 'launchd', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linkTargetT1608Description', + { defaultMessage: 'Link Target (T1608.005)' } + ), + id: 'T1608.005', + name: 'Link Target', + reference: 'https://attack.mitre.org/techniques/T1608/005', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'linkTarget', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description', @@ -8120,6 +8464,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1204', value: 'maliciousFile', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousImageT1204Description', + { defaultMessage: 'Malicious Image (T1204.003)' } + ), + id: 'T1204.003', + name: 'Malicious Image', + reference: 'https://attack.mitre.org/techniques/T1204/003', + tactics: 'execution', + techniqueId: 'T1204', + value: 'maliciousImage', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousLinkT1204Description', @@ -8156,6 +8512,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1588', value: 'malware', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.markOfTheWebBypassT1553Description', + { defaultMessage: 'Mark-of-the-Web Bypass (T1553.005)' } + ), + id: 'T1553.005', + name: 'Mark-of-the-Web Bypass', + reference: 'https://attack.mitre.org/techniques/T1553/005', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'markOfTheWebBypass', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.masqueradeTaskOrServiceT1036Description', @@ -8272,7 +8640,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.004', name: 'Network Device Authentication', reference: 'https://attack.mitre.org/techniques/T1556/004', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'networkDeviceAuthentication', }, @@ -8524,7 +8892,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.002', name: 'Password Filter DLL', reference: 'https://attack.mitre.org/techniques/T1556/002', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'passwordFilterDll', }, @@ -8540,6 +8908,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1110', value: 'passwordGuessing', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordManagersT1555Description', + { defaultMessage: 'Password Managers (T1555.005)' } + ), + id: 'T1555.005', + name: 'Password Managers', + reference: 'https://attack.mitre.org/techniques/T1555/005', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'passwordManagers', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordSprayingT1110Description', @@ -8620,7 +9000,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.003', name: 'Pluggable Authentication Modules', reference: 'https://attack.mitre.org/techniques/T1556/003', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'pluggableAuthenticationModules', }, @@ -8816,6 +9196,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'python', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcScriptsT1037Description', + { defaultMessage: 'RC Scripts (T1037.004)' } + ), + id: 'T1037.004', + name: 'RC Scripts', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'rcScripts', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description', @@ -8840,18 +9232,6 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1542', value: 'rommoNkit', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description', - { defaultMessage: 'Rc.common (T1037.004)' } - ), - id: 'T1037.004', - name: 'Rc.common', - reference: 'https://attack.mitre.org/techniques/T1037/004', - tactics: 'persistence,privilege-escalation', - techniqueId: 'T1037', - value: 'rcCommon', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reOpenedApplicationsT1547Description', @@ -9032,6 +9412,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1565', value: 'runtimeDataManipulation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.samlTokensT1606Description', + { defaultMessage: 'SAML Tokens (T1606.002)' } + ), + id: 'T1606.002', + name: 'SAML Tokens', + reference: 'https://attack.mitre.org/techniques/T1606/002', + tactics: 'credential-access', + techniqueId: 'T1606', + value: 'samlTokens', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sidHistoryInjectionT1134Description', @@ -9800,6 +10192,42 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'unixShell', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.unixShellConfigurationModificationT1546Description', + { defaultMessage: 'Unix Shell Configuration Modification (T1546.004)' } + ), + id: 'T1546.004', + name: 'Unix Shell Configuration Modification', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'unixShellConfigurationModification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.uploadMalwareT1608Description', + { defaultMessage: 'Upload Malware (T1608.001)' } + ), + id: 'T1608.001', + name: 'Upload Malware', + reference: 'https://attack.mitre.org/techniques/T1608/001', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'uploadMalware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.uploadToolT1608Description', + { defaultMessage: 'Upload Tool (T1608.002)' } + ), + id: 'T1608.002', + name: 'Upload Tool', + reference: 'https://attack.mitre.org/techniques/T1608/002', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'uploadTool', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.userActivityBasedChecksT1497Description', @@ -9932,6 +10360,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1596', value: 'whois', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webCookiesT1606Description', + { defaultMessage: 'Web Cookies (T1606.001)' } + ), + id: 'T1606.001', + name: 'Web Cookies', + reference: 'https://attack.mitre.org/techniques/T1606/001', + tactics: 'credential-access', + techniqueId: 'T1606', + value: 'webCookies', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webPortalCaptureT1056Description', @@ -10016,6 +10456,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'windowsCommandShell', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsCredentialManagerT1555Description', + { defaultMessage: 'Windows Credential Manager (T1555.004)' } + ), + id: 'T1555.004', + name: 'Windows Credential Manager', + reference: 'https://attack.mitre.org/techniques/T1555/004', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'windowsCredentialManager', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsFileAndDirectoryPermissionsModificationT1222Description', @@ -10078,63 +10530,15 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.additionalCloudCredentialsT1098Description', - { defaultMessage: 'Additional Cloud Credentials (T1098.001)' } - ), - id: 'T1098.001', - name: 'Additional Cloud Credentials', - reference: 'https://attack.mitre.org/techniques/T1098/001', - tactics: 'persistence', - techniqueId: 'T1098', - value: 'additionalCloudCredentials', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyModificationT1484Description', - { defaultMessage: 'Group Policy Modification (T1484.001)' } - ), - id: 'T1484.001', - name: 'Group Policy Modification', - reference: 'https://attack.mitre.org/techniques/T1484/001', - tactics: 'defense-evasion,privilege-escalation', - techniqueId: 'T1484', - value: 'groupPolicyModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainTrustModificationT1484Description', - { defaultMessage: 'Domain Trust Modification (T1484.002)' } - ), - id: 'T1484.002', - name: 'Domain Trust Modification', - reference: 'https://attack.mitre.org/techniques/T1484/002', - tactics: 'defense-evasion,privilege-escalation', - techniqueId: 'T1484', - value: 'domainTrustModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webCookiesT1606Description', - { defaultMessage: 'Web Cookies (T1606.001)' } - ), - id: 'T1606.001', - name: 'Web Cookies', - reference: 'https://attack.mitre.org/techniques/T1606/001', - tactics: 'credential-access', - techniqueId: 'T1606', - value: 'webCookies', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.samlTokensT1606Description', - { defaultMessage: 'SAML Tokens (T1606.002)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.xdgAutostartEntriesT1547Description', + { defaultMessage: 'XDG Autostart Entries (T1547.013)' } ), - id: 'T1606.002', - name: 'SAML Tokens', - reference: 'https://attack.mitre.org/techniques/T1606/002', - tactics: 'credential-access', - techniqueId: 'T1606', - value: 'samlTokens', + id: 'T1547.013', + name: 'XDG Autostart Entries', + reference: 'https://attack.mitre.org/techniques/T1547/013', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'xdgAutostartEntries', }, ]; @@ -10145,21 +10549,21 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ */ export const getMockThreatData = () => ({ tactic: { - name: 'Privilege Escalation', - id: 'TA0004', - reference: 'https://attack.mitre.org/tactics/TA0004', + name: 'Credential Access', + id: 'TA0006', + reference: 'https://attack.mitre.org/tactics/TA0006', }, technique: { - name: 'Event Triggered Execution', - id: 'T1546', - reference: 'https://attack.mitre.org/techniques/T1546', - tactics: ['privilege-escalation', 'persistence'], + name: 'OS Credential Dumping', + id: 'T1003', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: ['credential-access'], }, subtechnique: { - name: '.bash_profile and .bashrc', - id: 'T1546.004', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: ['privilege-escalation', 'persistence'], - techniqueId: 'T1546', + name: '/etc/passwd and /etc/shadow', + id: 'T1003.008', + reference: 'https://attack.mitre.org/techniques/T1003/008', + tactics: ['credential-access'], + techniqueId: 'T1003', }, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index f7e4ebd4daf1..323ef93133e2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiWindowEvent, + EuiHorizontalRule, +} from '@elastic/eui'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { isTab } from '../../../../../timelines/public'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; @@ -44,9 +52,11 @@ import { resetKeyboardFocus, showGlobalFilters, } from '../../../timelines/components/timeline/helpers'; -import { timelineSelectors } from '../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { + buildAlertStatusFilter, + buildAlertStatusFilterRuleRegistry, buildShowBuildingBlockFilter, buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, @@ -58,6 +68,10 @@ import { MissingPrivilegesCallOut } from '../../components/callouts/missing_priv import { useKibana } from '../../../common/lib/kibana'; import { AlertsCountPanel } from '../../components/alerts_kpis/alerts_count_panel'; import { CHART_HEIGHT } from '../../components/alerts_kpis/common/config'; +import { + AlertsTableFilterGroup, + FILTER_OPEN, +} from '../../components/alerts_table/alerts_filter_group'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -68,7 +82,13 @@ const StyledFullHeightContainer = styled.div` flex: 1 1 auto; `; -const DetectionEnginePageComponent = () => { +type DetectionEngineComponentProps = PropsFromRedux; + +const DetectionEnginePageComponent: React.FC = ({ + clearEventsDeleted, + clearEventsLoading, + clearSelected, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -108,6 +128,7 @@ const DetectionEnginePageComponent = () => { const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const { navigateToUrl } = useKibana().services.application; + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -134,23 +155,51 @@ const DetectionEnginePageComponent = () => { [formatUrl, navigateToUrl] ); + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: Status) => { + const timelineId = TimelineId.detectionsPage; + clearEventsLoading!({ id: timelineId }); + clearEventsDeleted!({ id: timelineId }); + clearSelected!({ id: timelineId }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + const alertsHistogramDefaultFilters = useMemo( () => [ ...filters, ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ...buildAlertStatusFilterRuleRegistry(filterGroup), + ] + : [ + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildAlertStatusFilter(filterGroup), + ]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ + filters, + ruleRegistryEnabled, + showBuildingBlockAlerts, + showOnlyThreatIndicatorAlerts, + filterGroup, + ] ); // AlertsTable manages global filters itself, so not including `filters` const alertsTableDefaultFilters = useMemo( () => [ ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ] + : [...buildShowBuildingBlockFilter(showBuildingBlockAlerts)]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], [ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] @@ -254,6 +303,9 @@ const DetectionEnginePageComponent = () => { {i18n.BUTTON_MANAGE_RULES} + + + { showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback} to={to} + filterGroup={filterGroup} /> @@ -304,4 +357,16 @@ const DetectionEnginePageComponent = () => { ); }; -export const DetectionEnginePage = React.memo(DetectionEnginePageComponent); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const DetectionEnginePage = connector(React.memo(DetectionEnginePageComponent)); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 233189a3e8be..230eaeb10939 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { @@ -31,6 +31,7 @@ import { ExceptionListIdentifiers, } from '@kbn/securitysolution-io-ts-list-types'; +import { Dispatch } from 'redux'; import { isTab } from '../../../../../../../timelines/public'; import { useDeepEqualSelector, @@ -63,6 +64,8 @@ import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; import { buildAlertsRuleIdFilter, + buildAlertStatusFilter, + buildAlertStatusFilterRuleRegistry, buildShowBuildingBlockFilter, buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, @@ -98,7 +101,7 @@ import { resetKeyboardFocus, showGlobalFilters, } from '../../../../../timelines/components/timeline/helpers'; -import { timelineSelectors } from '../../../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; @@ -118,6 +121,11 @@ import { MissingPrivilegesCallOut } from '../../../../components/callouts/missin import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { BadgeOptions } from '../../../../../common/components/header_page/types'; import { AlertsStackByField } from '../../../../components/alerts_kpis/common/types'; +import { Status } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { + AlertsTableFilterGroup, + FILTER_OPEN, +} from '../../../../components/alerts_table/alerts_filter_group'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -155,7 +163,13 @@ const ruleDetailTabs = [ }, ]; -const RuleDetailsPageComponent = () => { +type DetectionEngineComponentProps = PropsFromRedux; + +const RuleDetailsPageComponent: React.FC = ({ + clearEventsDeleted, + clearEventsLoading, + clearSelected, +}) => { const { navigateToApp } = useKibana().services.application; const dispatch = useDispatch(); const containerElement = useRef(null); @@ -226,6 +240,7 @@ const RuleDetailsPageComponent = () => { const mlCapabilities = useMlCapabilities(); const { formatUrl } = useFormatUrl(SecurityPageName.rules); const { globalFullScreen } = useGlobalFullScreen(); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); @@ -315,6 +330,18 @@ const RuleDetailsPageComponent = () => { [rule, ruleLoading] ); + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: Status) => { + const timelineId = TimelineId.detectionsPage; + clearEventsLoading!({ id: timelineId }); + clearEventsDeleted!({ id: timelineId }); + clearSelected!({ id: timelineId }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts useEffect(() => { setShowBuildingBlockAlerts(rule?.building_block_type != null); @@ -324,11 +351,38 @@ const RuleDetailsPageComponent = () => { () => [ ...buildAlertsRuleIdFilter(ruleId), ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), // TODO: Once we are past experimental phase this code should be removed + ...buildAlertStatusFilterRuleRegistry(filterGroup), + ] + : [ + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildAlertStatusFilter(filterGroup), + ]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ + ruleId, + ruleRegistryEnabled, + showBuildingBlockAlerts, + showOnlyThreatIndicatorAlerts, + filterGroup, + ] + ); + + const alertsTableDefaultFilters = useMemo( + () => [ + ...buildAlertsRuleIdFilter(ruleId), + ...filters, + ...(ruleRegistryEnabled + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ] + : [...buildShowBuildingBlockFilter(showBuildingBlockAlerts)]), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), + ], + [ruleId, filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -705,6 +759,8 @@ const RuleDetailsPageComponent = () => { {ruleDetailTab === RuleDetailTabs.alerts && ( <> + + { {ruleId != null && ( { ); }; +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; -export const RuleDetailsPage = React.memo(RuleDetailsPageComponent); +export const RuleDetailsPage = connector(React.memo(RuleDetailsPageComponent)); RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 09b47b76c486..22192a8a349d 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -5,24 +5,19 @@ * 2.0. */ -import React, { FC, memo } from 'react'; -import { EuiPanel, CommonProps } from '@elastic/eui'; -import styled from 'styled-components'; +import React, { FC, memo, useMemo } from 'react'; +import { + CommonProps, + EuiPageHeader, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; import { SecurityPageName } from '../../../common/constants'; -import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { HeaderPage } from '../../common/components/header_page'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { BETA_BADGE_LABEL } from '../common/translations'; - -/** Ensure that all flyouts z-index in Administation area show the flyout header */ -const EuiPanelStyled = styled(EuiPanel)` - .euiFlyout { - z-index: ${({ theme }) => theme.eui.euiZNavigation + 1}; - } -`; interface AdministrationListPageProps { - beta: boolean; title: React.ReactNode; subtitle: React.ReactNode; actions?: React.ReactNode; @@ -30,25 +25,41 @@ interface AdministrationListPageProps { } export const AdministrationListPage: FC = memo( - ({ beta, title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { - const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL }; + ({ title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { + const header = useMemo(() => { + return ( + + + {headerBackComponent && <>{headerBackComponent}} + + + + {title} + + + + ); + }, [headerBackComponent, title]); - return ( - - - {actions} - + const description = useMemo(() => { + return {subtitle}; + }, [subtitle]); - {children} + return ( + <> + + + {children} - + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index 1cbd45633b91..e5ae0bc8aa4d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -61,6 +61,7 @@ export const AdminSearchBar = memo(() => { timeHistory={timeHistory} onQuerySubmit={onQuerySubmit} isLoading={false} + iconType="search" showFilterBar={false} showDatePicker={false} showQueryBar={true} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9f72f64d3efc..c054f7d06dde 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -25,7 +25,7 @@ import { HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { POLICY_STATUS_TO_TEXT } from './host_constants'; +import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/test_mock_utils'; import { getEndpointDetailsPath } from '../../../common/routing'; import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana'; @@ -337,7 +337,7 @@ describe('when on the endpoint list page', () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); const total = await renderResult.findByTestId('endpointListTableTotal'); - expect(total.textContent).toEqual('5 Hosts'); + expect(total.textContent).toEqual('Showing 5 endpoints'); }); it('should display correct status', async () => { const renderResult = render(); @@ -380,18 +380,14 @@ describe('when on the endpoint list page', () => { const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { - const policyStatusToRGBColor: Array<[string, string]> = [ - ['Success', 'background-color: rgb(109, 204, 177);'], - ['Warning', 'background-color: rgb(241, 216, 111);'], - ['Failure', 'background-color: rgb(255, 126, 98);'], - ['Unsupported', 'background-color: rgb(211, 218, 230);'], - ]; - const policyStatusStyleMap: ReadonlyMap = new Map( - policyStatusToRGBColor - ); - const expectedStatusColor: string = policyStatusStyleMap.get(status.textContent!) ?? ''; expect(status.textContent).toEqual(POLICY_STATUS_TO_TEXT[generatedPolicyStatuses[index]]); - expect(status.getAttribute('style')).toMatch(expectedStatusColor); + expect( + status.querySelector( + `[data-euiicon-type][color=${ + POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]] + }]` + ) + ).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 9b745039cac8..8af1498c2431 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React, { useMemo, useCallback, memo, useContext, useEffect, useState } from 'react'; +import React, { useMemo, useCallback, memo, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { EuiHorizontalRule, EuiBasicTable, EuiBasicTableColumn, EuiText, EuiLink, - EuiBadge, + EuiHealth, EuiToolTip, EuiSelectableProps, EuiSuperDatePicker, @@ -26,12 +27,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { useDispatch } from 'react-redux'; -import { ThemeContext } from 'styled-components'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { useEndpointSelector } from './hooks'; import { isPolicyOutOfDate } from '../utils'; -import { POLICY_STATUS_TO_BADGE_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; @@ -64,32 +64,28 @@ import { metadataTransformPrefix } from '../../../../../common/endpoint/constant const MAX_PAGINATED_ITEM = 9999; const TRANSFORM_URL = '/data/transform'; +const StyledDatePicker = styled.div` + .euiFormControlLayout--group { + background-color: rgba(0, 119, 204, 0.2); + } + + .euiDatePickerRange--readOnly { + background-color: ${(props) => props.theme.eui.euiFormBackgroundColor}; + } +`; const EndpointListNavLink = memo<{ name: string; href: string; route: string; - isBadge?: boolean; dataTestSubj: string; -}>(({ name, href, route, isBadge = false, dataTestSubj }) => { +}>(({ name, href, route, dataTestSubj }) => { const clickHandler = useNavigateByRouterEventHandler(route); - const theme = useContext(ThemeContext); - return isBadge ? ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - {name} - - ) : ( + return ( // eslint-disable-next-line @elastic/eui/href-or-on-click @@ -269,7 +265,7 @@ export const EndpointList = () => { const columns: Array>> = useMemo(() => { const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', { - defaultMessage: 'Last Active', + defaultMessage: 'Last active', }); return [ @@ -277,7 +273,7 @@ export const EndpointList = () => { field: 'metadata', width: '15%', name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', { - defaultMessage: 'Hostname', + defaultMessage: 'Endpoint', }), render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => { const toRoutePath = getEndpointDetailsPath( @@ -305,7 +301,7 @@ export const EndpointList = () => { field: 'host_status', width: '14%', name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', { - defaultMessage: 'Agent Status', + defaultMessage: 'Agent status', }), // eslint-disable-next-line react/display-name render: (hostStatus: HostInfo['host_status'], endpointInfo) => { @@ -318,7 +314,7 @@ export const EndpointList = () => { field: 'metadata.Endpoint.policy.applied', width: '15%', name: i18n.translate('xpack.securitySolution.endpoint.list.policy', { - defaultMessage: 'Integration Policy', + defaultMessage: 'Policy', }), truncateText: true, // eslint-disable-next-line react/display-name @@ -339,6 +335,7 @@ export const EndpointList = () => { color="subdued" size="xs" style={{ whiteSpace: 'nowrap', ...PAD_LEFT }} + className="eui-textTruncate" data-test-subj="policyListRevNo" > { field: 'metadata.Endpoint.policy.applied', width: '9%', name: i18n.translate('xpack.securitySolution.endpoint.list.policyStatus', { - defaultMessage: 'Policy Status', + defaultMessage: 'Policy status', }), render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getEndpointDetailsPath({ @@ -369,19 +366,23 @@ export const EndpointList = () => { }); const toRouteUrl = getAppUrl({ path: toRoutePath }); return ( - - - + + + + ); }, }, @@ -389,24 +390,36 @@ export const EndpointList = () => { field: 'metadata.host.os.name', width: '9%', name: i18n.translate('xpack.securitySolution.endpoint.list.os', { - defaultMessage: 'Operating System', + defaultMessage: 'OS', }), - truncateText: true, + // eslint-disable-next-line react/display-name + render: (os: string) => { + return ( + + +

{os}

+
+
+ ); + }, }, { field: 'metadata.host.ip', width: '12%', name: i18n.translate('xpack.securitySolution.endpoint.list.ip', { - defaultMessage: 'IP Address', + defaultMessage: 'IP address', }), // eslint-disable-next-line react/display-name render: (ip: string[]) => { return ( - - - + + +

{ip.toString().replace(',', ', ')} - +

); @@ -418,6 +431,16 @@ export const EndpointList = () => { name: i18n.translate('xpack.securitySolution.endpoint.list.endpointVersion', { defaultMessage: 'Version', }), + // eslint-disable-next-line react/display-name + render: (version: string) => { + return ( + + +

{version}

+
+
+ ); + }, }, { field: 'metadata.@timestamp', @@ -592,7 +615,6 @@ export const EndpointList = () => { return ( { subtitle={ } > @@ -647,15 +669,18 @@ export const EndpointList = () => { )} - + + + @@ -666,13 +691,13 @@ export const EndpointList = () => { {totalItemCount > MAX_PAGINATED_ITEM + 1 ? ( ) : ( )} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 3c537320bc92..8466e19100f7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -188,7 +188,6 @@ export const EventFiltersListPage = memo(() => { return ( >; }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); @@ -93,10 +95,9 @@ export const ProtectionSwitch = React.memo( return ( { const OSes: Immutable = [OS.windows, OS.mac, OS.linux]; const protection = 'behavior_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.behavior', + { + defaultMessage: 'Behaviour protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]} dataTestSubj="behaviorProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 6374ba3bc4f5..5056cd2feb10 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -26,6 +26,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const MalwareProtections = React.memo(() => { const OSes: Immutable = [OS.windows, OS.mac, OS.linux]; const protection = 'malware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.malware', + { + defaultMessage: 'Malware protections', + } + ); const isPlatinumPlus = useLicense().isPlatinumPlus(); return ( @@ -35,7 +41,9 @@ export const MalwareProtections = React.memo(() => { })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]} dataTestSubj="malwareProtectionsForm" - rightCorner={} + rightCorner={ + + } > {isPlatinumPlus && } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx index 45d472a00f31..f12449fccbe2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx @@ -25,6 +25,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const MemoryProtection = React.memo(() => { const OSes: Immutable = [OS.windows]; const protection = 'memory_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.memory', + { + defaultMessage: 'Memory protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS]} dataTestSubj="memoryProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx index 70f41015bc25..96635482f4b6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx @@ -25,6 +25,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const Ransomware = React.memo(() => { const OSes: Immutable = [OS.windows]; const protection = 'ransomware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.ransomware', + { + defaultMessage: 'Ransomware protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS]} dataTestSubj="ransomwareProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 48ff54a0e3b5..de4384b2fac4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -164,7 +164,6 @@ export const TrustedAppsPage = memo(() => { return ( { +): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy: NewPackagePolicy, context: RequestHandlerContext, @@ -101,7 +104,7 @@ export const getPackagePolicyCreateCallback = ( export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService -): ExternalCallback[1] => { +): PutPackagePolicyUpdateCallback => { return async ( newPackagePolicy: NewPackagePolicy // context: RequestHandlerContext, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 80ae8b9309f1..f07bed9fa556 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -1583,10 +1583,6 @@ Object { "path": "signal.ancestors.type", "type": "alias", }, - "kibana.alert.consumers": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.depth": Object { "path": "signal.depth", "type": "alias", @@ -1643,6 +1639,10 @@ Object { "path": "signal.original_event.provider", "type": "alias", }, + "kibana.alert.original_event.reason": Object { + "path": "signal.original_event.reason", + "type": "alias", + }, "kibana.alert.original_event.risk_score": Object { "path": "signal.original_event.risk_score", "type": "alias", @@ -1675,9 +1675,9 @@ Object { "path": "signal.original_time", "type": "alias", }, - "kibana.alert.producer": Object { - "type": "constant_keyword", - "value": "siem", + "kibana.alert.reason": Object { + "path": "signal.reason", + "type": "alias", }, "kibana.alert.risk_score": Object { "path": "signal.rule.risk_score", @@ -1691,6 +1691,10 @@ Object { "path": "signal.rule.building_block_type", "type": "alias", }, + "kibana.alert.rule.consumer": Object { + "type": "constant_keyword", + "value": "siem", + }, "kibana.alert.rule.created_at": Object { "path": "signal.rule.created_at", "type": "alias", @@ -1751,6 +1755,10 @@ Object { "path": "signal.rule.note", "type": "alias", }, + "kibana.alert.rule.producer": Object { + "type": "constant_keyword", + "value": "siem", + }, "kibana.alert.rule.query": Object { "path": "signal.rule.query", "type": "alias", @@ -3249,6 +3257,9 @@ Object { "provider": Object { "type": "keyword", }, + "reason": Object { + "type": "keyword", + }, "risk_score": Object { "type": "float", }, @@ -3318,6 +3329,9 @@ Object { }, }, }, + "reason": Object { + "type": "keyword", + }, "rule": Object { "properties": Object { "author": Object { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 88c549cec557..3355b0659f28 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -109,8 +109,8 @@ describe('get_signals_template', () => { const constantKeywordsFound = recursiveConstantKeywordFound('', template); expect(constantKeywordsFound).toEqual([ 'template.mappings.properties.kibana.space_ids', - 'template.mappings.properties.kibana.alert.consumers', - 'template.mappings.properties.kibana.alert.producer', + 'template.mappings.properties.kibana.alert.rule.consumer', + 'template.mappings.properties.kibana.alert.rule.producer', 'template.mappings.properties.kibana.alert.rule.rule_type_id', ]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index bc41441e1a11..989c73f97997 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -7,8 +7,8 @@ import { SPACE_IDS, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_RULE_TYPE_ID, } from '@kbn/rule-data-utils'; import signalsMapping from './signals_mapping.json'; @@ -116,11 +116,11 @@ export const getRbacRequiredFields = (spaceId: string) => { type: 'constant_keyword', value: spaceId, }, - [ALERT_CONSUMERS]: { + [ALERT_RULE_CONSUMER]: { type: 'constant_keyword', value: 'siem', }, - [ALERT_PRODUCER]: { + [ALERT_RULE_PRODUCER]: { type: 'constant_keyword', value: 'siem', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json index 066fdbc87f90..68c184b66c56 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json @@ -17,6 +17,7 @@ "signal.original_event.module": "kibana.alert.original_event.module", "signal.original_event.outcome": "kibana.alert.original_event.outcome", "signal.original_event.provider": "kibana.alert.original_event.provider", + "signal.original_event.reason": "kibana.alert.original_event.reason", "signal.original_event.risk_score": "kibana.alert.original_event.risk_score", "signal.original_event.risk_score_norm": "kibana.alert.original_event.risk_score_norm", "signal.original_event.sequence": "kibana.alert.original_event.sequence", @@ -25,6 +26,7 @@ "signal.original_event.timezone": "kibana.alert.original_event.timezone", "signal.original_event.type": "kibana.alert.original_event.type", "signal.original_time": "kibana.alert.original_time", + "signal.reason": "kibana.alert.reason", "signal.rule.author": "kibana.alert.rule.author", "signal.rule.building_block_type": "kibana.alert.rule.building_block_type", "signal.rule.created_at": "kibana.alert.rule.created_at", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json index e20aa0ef16df..7bc20fd540b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json @@ -43,6 +43,9 @@ } } }, + "reason": { + "type": "keyword" + }, "rule": { "type": "object", "properties": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index d6a06848592c..4f754ecd2d33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -360,6 +360,9 @@ "provider": { "type": "keyword" }, + "reason": { + "type": "keyword" + }, "risk_score": { "type": "float" }, @@ -421,6 +424,9 @@ }, "depth": { "type": "integer" + }, + "reason": { + "type": "keyword" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts index 700ce66e2770..b3c70cd56d9e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts @@ -18,9 +18,6 @@ import { * @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation */ export const ruleExecutionFieldMap = { - // [ALERT_OWNER]: { type: 'keyword', required: true }, - // [SPACE_IDS]: { type: 'keyword', array: true, required: true }, - // [RULE_ID]: { type: 'keyword', required: true }, [MESSAGE]: { type: 'keyword' }, [EVENT_SEQUENCE]: { type: 'long' }, [EVENT_END]: { type: 'date' }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts index f5971475e2b1..1c19859eef43 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts @@ -7,12 +7,13 @@ import { estypes } from '@elastic/elasticsearch'; import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, EVENT_ACTION, EVENT_KIND, - RULE_ID, SPACE_IDS, TIMESTAMP, + ALERT_RULE_ID, } from '@kbn/rule-data-utils'; import { once } from 'lodash/fp'; import moment from 'moment'; @@ -95,7 +96,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { } const filter: estypes.QueryDslQueryContainer[] = [ - { terms: { [RULE_ID]: ruleIds } }, + { terms: { [ALERT_RULE_ID]: ruleIds } }, { terms: { [SPACE_IDS]: [spaceId] } }, ]; @@ -114,7 +115,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { aggs: { rules: { terms: { - field: RULE_ID, + field: ALERT_RULE_ID, size: ruleIds.length, }, aggs: { @@ -147,7 +148,10 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { bucket.key, bucket.most_recent_logs.hits.hits.map((event) => { const logEntry = parseRuleExecutionLog(event._source); - invariant(logEntry['rule.id'], 'Malformed execution log entry: rule.id field not found'); + invariant( + logEntry[ALERT_RULE_ID] ?? '', + 'Malformed execution log entry: rule.id field not found' + ); const lastFailure = bucket.last_failure.event.hits.hits[0] ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) @@ -179,7 +183,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { ] : undefined; - const alertId = logEntry['rule.id']; + const alertId = logEntry[ALERT_RULE_ID] ?? ''; const statusDate = logEntry[TIMESTAMP]; const lastFailureAt = lastFailure?.[TIMESTAMP]; const lastFailureMessage = lastFailure?.[MESSAGE]; @@ -213,14 +217,6 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { ); } - // { [x: string]: string | string[] | ExecutionMetricValue; - // [x: number]: string; - // "kibana.space_ids": string[]; - // "event.action": T; - // "event.kind": string; - // "rule.id": string; - // "@timestamp": string; } - public async logExecutionMetric({ ruleId, namespace, @@ -234,9 +230,10 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_ACTION]: metric, [EVENT_KIND]: 'metric', [getMetricField(metric)]: value, - [RULE_ID]: ruleId, + [ALERT_RULE_ID]: ruleId ?? '', [TIMESTAMP]: new Date().toISOString(), - [ALERT_OWNER]: 'siem', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, }, namespace ); @@ -256,11 +253,12 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_KIND]: 'event', [EVENT_SEQUENCE]: this.sequence++, [MESSAGE]: message, - [RULE_ID]: ruleId, + [ALERT_RULE_ID]: ruleId ?? '', [RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus], [RULE_STATUS]: newStatus, [TIMESTAMP]: new Date().toISOString(), - [ALERT_OWNER]: 'siem', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, }, namespace ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index f9874478e7a5..09f35e279a24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -6,7 +6,8 @@ */ import { - ALERT_OWNER, + ALERT_REASON, + ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, ALERT_WORKFLOW_STATUS, @@ -50,15 +51,16 @@ describe('buildAlert', () => { const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; const expected = { '@timestamp': timestamp, [SPACE_IDS]: [SPACE_ID], - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', @@ -68,6 +70,7 @@ describe('buildAlert', () => { }, ], [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z', + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { @@ -119,15 +122,16 @@ describe('buildAlert', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; const expected = { '@timestamp': timestamp, [SPACE_IDS]: [SPACE_ID], - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', @@ -143,6 +147,7 @@ describe('buildAlert', () => { kind: 'event', module: 'system', }, + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 641b37cb54bc..eea85ba26faf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -6,7 +6,8 @@ */ import { - ALERT_OWNER, + ALERT_REASON, + ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, ALERT_WORKFLOW_STATUS, @@ -92,7 +93,8 @@ export const removeClashes = (doc: SimpleHit) => { export const buildAlert = ( docs: SimpleHit[], rule: RulesSchema, - spaceId: string | null | undefined + spaceId: string | null | undefined, + reason: string ): RACAlert => { const removedClashes = docs.map(removeClashes); const parents = removedClashes.map(buildParent); @@ -104,12 +106,13 @@ export const buildAlert = ( return ({ '@timestamp': new Date().toISOString(), - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [SPACE_IDS]: spaceId != null ? [spaceId] : [], [ALERT_ANCESTORS]: ancestors, [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_DEPTH]: depth, + [ALERT_REASON]: reason, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, rule), } as unknown) as RACAlert; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ca5857e0ee39..a67337d3b779 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -9,6 +9,7 @@ import { SavedObject } from 'src/core/types'; import { BaseHit } from '../../../../../../common/detection_engine/types'; import type { ConfigType } from '../../../../../config'; import { buildRuleWithOverrides, buildRuleWithoutOverrides } from '../../../signals/build_rule'; +import { BuildReasonMessage } from '../../../signals/reason_formatters'; import { getMergeStrategy } from '../../../signals/source_fields_merging/strategies'; import { AlertAttributes, SignalSource, SignalSourceHit } from '../../../signals/types'; import { RACAlert } from '../../types'; @@ -35,19 +36,23 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], - applyOverrides: boolean + applyOverrides: boolean, + buildReasonMessage: BuildReasonMessage ): RACAlert => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}) : buildRuleWithoutOverrides(ruleSO); const filteredSource = filterSource(mergedDoc); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); if (isSourceDoc(mergedDoc)) { return { ...filteredSource, - ...buildAlert([mergedDoc], rule, spaceId), + ...buildAlert([mergedDoc], rule, spaceId, reason), ...additionalAlertFields(mergedDoc), - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 0b00b2f65637..62946c52b7f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; mergeStrategy: ConfigType['alertMergeStrategy']; spaceId: string | null | undefined; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { try { const wrappedDocs: WrappedRACAlert[] = events.flatMap((doc) => [ { @@ -35,7 +35,14 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(spaceId, ruleSO, doc as SignalSourceHit, mergeStrategy, true), + _source: buildBulkBody( + spaceId, + ruleSO, + doc as SignalSourceHit, + mergeStrategy, + true, + buildReasonMessage + ), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 7ab998fe1607..1c4b7f03fd73 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -193,6 +193,11 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, + 'kibana.alert.reason': { + type: 'keyword', + array: false, + required: false, + }, 'kibana.alert.threat': { type: 'object', array: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 117dcdf0c18d..206f3ae59d24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -37,11 +37,13 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -77,6 +79,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -91,6 +94,7 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body with threshold results', () => { const ruleSO = sampleRuleSO(getThresholdRuleParams()); const baseDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const doc: SignalSourceHit & { _source: Required['_source'] } = { ...baseDoc, _source: { @@ -109,7 +113,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -145,6 +150,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: { ...expectedRule(), @@ -181,6 +187,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -191,7 +198,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -227,6 +235,7 @@ describe('buildBulkBody', () => { depth: 0, }, ], + reason: 'reasonable reason', ancestors: [ { id: sampleIdGuid, @@ -250,6 +259,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -259,7 +269,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -303,6 +314,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -317,6 +329,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { kind: 'event', @@ -324,7 +337,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -363,6 +377,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -377,6 +392,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as a numeric', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -388,7 +404,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -423,6 +440,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -437,6 +455,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as an object', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -448,7 +467,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -483,6 +503,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -504,7 +525,12 @@ describe('buildSignalFromSequence', () => { block2._source.new_key = 'new_key_value'; const blocks = [block1, block2]; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence(blocks, ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + blocks, + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit & { new_key: string } = { @@ -573,6 +599,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -589,7 +616,12 @@ describe('buildSignalFromSequence', () => { block2._source['@timestamp'] = '2021-05-20T22:28:46+0000'; block2._source.someKey = 'someOtherValue'; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence([block1, block2], ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + [block1, block2], + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit = { @@ -657,6 +689,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -673,11 +706,13 @@ describe('buildSignalFromEvent', () => { const ancestor = sampleDocWithAncestors().hits.hits[0]; delete ancestor._source.source; const ruleSO = sampleRuleSO(getQueryRuleParams()); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const signal: SignalHitOptionalTimestamp = buildSignalFromEvent( ancestor, ruleSO, true, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -724,6 +759,7 @@ describe('buildSignalFromEvent', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 54a41be5cbad..626dcb2fe83f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -22,6 +22,7 @@ import { buildEventTypeSignal } from './build_event_type_signal'; import { EqlSequence } from '../../../../common/detection_engine/types'; import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils'; import type { ConfigType } from '../../../config'; +import { BuildReasonMessage } from './reason_formatters'; /** * Formats the search_after result for insertion into the signals index. We first create a @@ -35,12 +36,15 @@ import type { ConfigType } from '../../../config'; export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedDoc], rule), + ...buildSignal([mergedDoc], rule, reason), ...additionalSignalFields(mergedDoc), }; const event = buildEventTypeSignal(mergedDoc); @@ -52,7 +56,7 @@ export const buildBulkBody = ( }; const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event, signal, }; @@ -71,11 +75,12 @@ export const buildSignalGroupFromSequence = ( sequence: EqlSequence, ruleSO: SavedObject, outputIndex: string, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): WrappedSignalHit[] => { const wrappedBuildingBlocks = wrapBuildingBlocks( sequence.events.map((event) => { - const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy); + const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy, buildReasonMessage); signal.signal.rule.building_block_type = 'default'; return signal; }), @@ -94,7 +99,7 @@ export const buildSignalGroupFromSequence = ( // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block const sequenceSignal = wrapSignal( - buildSignalFromSequence(wrappedBuildingBlocks, ruleSO), + buildSignalFromSequence(wrappedBuildingBlocks, ruleSO, buildReasonMessage), outputIndex ); wrappedBuildingBlocks.forEach((block, idx) => { @@ -111,14 +116,18 @@ export const buildSignalGroupFromSequence = ( export const buildSignalFromSequence = ( events: WrappedSignalHit[], - ruleSO: SavedObject + ruleSO: SavedObject, + buildReasonMessage: BuildReasonMessage ): SignalHit => { const rule = buildRuleWithoutOverrides(ruleSO); - const signal: Signal = buildSignal(events, rule); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ rule, timestamp }); + const signal: Signal = buildSignal(events, rule, reason); const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); return { ...mergedEvents, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: { kind: 'signal', }, @@ -137,14 +146,17 @@ export const buildSignalFromEvent = ( event: BaseSignalHit, ruleSO: SavedObject, applyOverrides: boolean, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {}) : buildRuleWithoutOverrides(ruleSO); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc: mergedEvent, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedEvent], rule), + ...buildSignal([mergedEvent], rule, reason), ...additionalSignalFields(mergedEvent), }; const eventFields = buildEventTypeSignal(mergedEvent); @@ -155,7 +167,7 @@ export const buildSignalFromEvent = ( // TODO: better naming for SignalHit - it's really a new signal to be inserted const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: eventFields, signal, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 8c0790761a5e..90b9cce9e057 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -31,8 +31,10 @@ describe('buildSignal', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; + const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -62,6 +64,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', status: 'open', rule: { author: [], @@ -112,8 +115,9 @@ describe('buildSignal', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -143,6 +147,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', original_event: { action: 'socket_opened', dataset: 'socket', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index a415c83e857c..962869cc4d61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -77,7 +77,7 @@ export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { * @param docs The parent signals/events of the new signal to be built. * @param rule The rule that is generating the new signal. */ -export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => { +export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema, reason: string): Signal => { const _meta = { version: SIGNALS_TEMPLATE_VERSION, }; @@ -94,6 +94,7 @@ export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => ancestors, status: 'open', rule, + reason, depth, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index ebb4462817ea..be6f4cb8feae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -19,6 +19,7 @@ import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; import { AlertAttributes, BulkCreate, WrapHits } from './types'; import { MachineLearningRuleParams } from '../schemas/rule_schemas'; +import { buildReasonMessageForMlAlert } from './reason_formatters'; interface BulkCreateMlSignalsParams { someResult: AnomalyResults; @@ -89,6 +90,6 @@ export const bulkCreateMlSignals = async ( const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - const wrappedDocs = params.wrapHits(ecsResults.hits.hits); + const wrappedDocs = params.wrapHits(ecsResults.hits.hits, buildReasonMessageForMlAlert); return params.bulkCreate(wrappedDocs); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 8d19510c6347..9a2805610ca8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -35,6 +35,7 @@ import { } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForEqlAlert } from '../reason_formatters'; export const eqlExecutor = async ({ rule, @@ -119,9 +120,9 @@ export const eqlExecutor = async ({ result.searchAfterTimes = [eqlSearchDuration]; let newSignals: SimpleHit[] | undefined; if (response.hits.sequences !== undefined) { - newSignals = wrapSequences(response.hits.sequences); + newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert); } else if (response.hits.events !== undefined) { - newSignals = wrapHits(response.hits.events); + newSignals = wrapHits(response.hits.events, buildReasonMessageForEqlAlert); } else { throw new Error( 'eql query response should have either `sequences` or `events` but had neither' diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index f27680315d19..f281475fe59e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -22,6 +22,7 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForQueryAlert } from '../reason_formatters'; export const queryExecutor = async ({ rule, @@ -84,6 +85,7 @@ export const queryExecutor = async ({ signalsIndex: ruleParams.outputIndex, filter: esFilter, pageSize: searchAfterSize, + buildReasonMessage: buildReasonMessageForQueryAlert, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts new file mode 100644 index 000000000000..e7f4fb41c763 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { buildCommonReasonMessage } from './reason_formatters'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +describe('reason_formatter', () => { + let rule: RulesSchema; + let mergedDoc: SignalSourceHit; + let timestamp: string; + beforeAll(() => { + rule = { + name: 'What is in a name', + risk_score: 9000, + severity: 'medium', + } as RulesSchema; // Cast here as all fields aren't required + mergedDoc = { + _index: 'some-index', + _id: 'some-id', + fields: { + 'host.name': ['party host'], + 'user.name': ['ferris bueller'], + '@timestamp': '2021-08-11T02:28:59.101Z', + }, + }; + timestamp = '2021-08-11T02:28:59.401Z'; + }); + + describe('buildCommonReasonMessage', () => { + describe('when rule, mergedDoc, and timestamp are provided', () => { + it('should return the full reason message', () => { + expect(buildCommonReasonMessage({ rule, mergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller on party host.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and host.name is missing', () => { + it('should return the reason message without the host name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'host.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and user.name is missing', () => { + it('should return the reason message without the user name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'user.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 on party host.' + ); + }); + }); + describe('when only rule and timestamp are provided', () => { + it('should return the reason message without host name or user name', () => { + expect(buildCommonReasonMessage({ rule, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000.' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts new file mode 100644 index 000000000000..0586462a2a58 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +export interface BuildReasonMessageArgs { + rule: RulesSchema; + mergedDoc?: SignalSourceHit; + timestamp: string; +} + +export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string; + +/** + * Currently all security solution rule types share a common reason message string. This function composes that string + * In the future there may be different configurations based on the different rule types, so the plumbing has been put in place + * to more easily allow for this in the future. + * @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here. + */ +export const buildCommonReasonMessage = ({ + rule, + mergedDoc, + timestamp, +}: BuildReasonMessageArgs) => { + if (!rule) { + // This should never happen, but in case, better to not show a malformed string + return ''; + } + let hostName; + let userName; + if (mergedDoc?.fields) { + hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName; + userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName; + } + + const isFieldEmpty = (field: string | string[] | undefined | null) => + !field || !field.length || (field.length === 1 && field[0] === '-'); + + return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', { + defaultMessage: + 'Alert {alertName} created at {timestamp} with a {alertSeverity} severity and risk score of {alertRiskScore}{userName, select, null {} other {{whitespace}by {userName}} }{hostName, select, null {} other {{whitespace}on {hostName}} }.', + values: { + alertName: rule.name, + alertSeverity: rule.severity, + alertRiskScore: rule.risk_score, + hostName: isFieldEmpty(hostName) ? 'null' : hostName, + timestamp, + userName: isFieldEmpty(userName) ? 'null' : userName, + whitespace: ' ', // there isn't support for the unicode /u0020 for whitespace, and leading spaces are deleted, so to prevent double-whitespace explicitly passing the space in. + }, + }); +}; + +export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md new file mode 100644 index 000000000000..059005707625 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md @@ -0,0 +1,144 @@ +This is where you add code when you have rules which contain saved object references. Saved object references are for +when you have "joins" in the saved objects between one saved object and another one. This can be a 1 to M (1 to many) +relationship for example where you have a rule which contains the "id" of another saved object. + +Examples are the `exceptionsList` on a rule which contains a saved object reference from the rule to another set of +saved objects of the type `exception-list` + +## Useful queries +How to get all your alerts to see if you have `exceptionsList` on it or not in dev tools: + +```json +GET .kibana/_search +{ + "query": { + "term": { + "type": { + "value": "alert" + } + } + } +} +``` + +## Structure on disk +Run a query in dev tools and you should see this code that adds the following savedObject references +to any newly saved rule: + +```json + { + "_index" : ".kibana-hassanabad19_8.0.0_001", + "_id" : "alert:38482620-ef1b-11eb-ad71-7de7959be71c", + "_score" : 6.2607274, + "_source" : { + "alert" : { + "name" : "kql test rule 1", + "tags" : [ + "__internal_rule_id:4ec223b9-77fa-4895-8539-6b3e586a2858", + "__internal_immutable:false" + ], + "alertTypeId" : "siem.signals", + "other data... other data": "other data...other data", + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], + "other data... other data": "other data...other data", + "references" : [ + { + "name" : "param:exceptionsList_0", + "id" : "endpoint_list", + "type" : "exception-list" + }, + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ], + "other data... other data": "other data...other data" + } + } + } +``` + +The structure is that the alerting framework in conjunction with this code will make an array of saved object references which are going to be: +```json +{ + "references" : [ + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ] +} +``` + +`name` is the pattern of `param:${name}_${index}`. See the functions and constants in `utils.ts` of: + +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +For how it is constructed and retrieved. If you need to add more types, you should copy and create your own versions or use the generic +utilities/helpers if possible. + +`id` is the saved object id and should always be the same value as the `"exceptionsList" : [ "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c" ...`. +If for some reason the saved object id changes or is different, then on the next save/persist the `exceptionsList.id` will update to that within +its saved object. Note though, that the references id replaces _always_ the `exceptionsList.id` at all times through `inject_references.ts`. If +for some reason the `references` id is deleted, then on the next `inject_references` it will prefer to use the last good known reference and log +a warning. + +Within the rule parameters you can still keep the last known good saved object reference id as above it is shown +```json +{ + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], +} +``` + +## How to add a new saved object id reference to a rule + +See the files of: +* extract_references.ts +* inject_references.ts + +And their top level comments for how to wire up new instances. It's best to create a new file per saved object reference and push only the needed data +per file. + +Good examples and utilities can be found in the folder of `utils` such as: +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +You can follow those patterns but if it doesn't fit your use case it's fine to just create a new file and wire up your new saved object references + +## End to end tests +At this moment there are none. \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts new file mode 100644 index 000000000000..56a1e875ac5e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extractExceptionsList } from './extract_exceptions_list'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('it returns an empty array given an empty array for exceptionsList', () => { + expect(extractExceptionsList({ logger, exceptionsList: [] })).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + extractExceptionsList({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + }); + + test('It returns exception list transformed into a saved object references', () => { + expect( + extractExceptionsList({ logger, exceptionsList: mockExceptionsList() }) + ).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); + + test('It returns two exception lists transformed into a saved object references', () => { + const twoInputs: RuleParams['exceptionsList'] = [ + mockExceptionsList()[0], + { ...mockExceptionsList()[0], id: '976' }, + ]; + expect(extractExceptionsList({ logger, exceptionsList: twoInputs })).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + { + id: '976', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts new file mode 100644 index 000000000000..9b7f8bbcefee --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { getSavedObjectNamePatternForExceptionsList } from './utils'; + +/** + * This extracts the "exceptionsList" "id" and returns it as a saved object reference. + * NOTE: Due to rolling upgrades with migrations and a few bugs with migrations, I do an additional check for if "exceptionsList" exists or not. Once + * those bugs are fixed, we can remove the "if (exceptionsList == null) {" check, but for the time being it is there to keep things running even + * if exceptionsList has not been migrated. + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to get the id from and return it as a saved object reference. + * @returns The saved object references from the exceptions list + */ +export const extractExceptionsList = ({ + logger, + exceptionsList, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; +}): SavedObjectReference[] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + return []; + } else { + return exceptionsList.map((exceptionItem, index) => ({ + name: getSavedObjectNamePatternForExceptionsList(index), + id: exceptionItem.id, + type: EXCEPTION_LIST_NAMESPACE, + })); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts new file mode 100644 index 000000000000..31288559e943 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { extractReferences } from './extract_references'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('It returns params untouched and the references extracted as exception list saved object references', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is missing for any reason', () => { + const params: Partial = { + note: 'some note', + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts new file mode 100644 index 000000000000..92e689e22576 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts @@ -0,0 +1,57 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { RuleParamsAndRefs } from '../../../../../../alerting/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { extractExceptionsList } from './extract_exceptions_list'; + +/** + * Extracts references and returns the saved object references. + * How to add a new extracted references here: + * --- + * Add a new file for extraction named: extract_.ts, example: extract_foo.ts + * Add a function into that file named: extract, example: extractFoo(logger, params.foo) + * Add a new line below and concat together the new extract with existing ones like so: + * + * const exceptionReferences = extractExceptionsList(logger, params.exceptionsList); + * const fooReferences = extractFoo(logger, params.foo); + * const returnReferences = [...exceptionReferences, ...fooReferences]; + * + * Optionally you can remove any parameters you do not want to store within the Saved Object here: + * const paramsWithoutSavedObjectReferences = { removeParam, ...otherParams }; + * + * If you do remove params, then update the types in: security_solution/server/lib/detection_engine/signals/types.ts + * to use an omit for the functions of "isAlertExecutor" and "SignalRuleAlertTypeDefinition" + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @returns The rule parameters and the saved object references to store. + */ +export const extractReferences = ({ + logger, + params, +}: { + logger: Logger; + params: RuleParams; +}): RuleParamsAndRefs => { + const exceptionReferences = extractExceptionsList({ + logger, + exceptionsList: params.exceptionsList, + }); + const returnReferences = [...exceptionReferences]; + + // Modify params if you want to remove any elements separately here. For exceptionLists, we do not remove the id and instead + // keep it to both fail safe guard against manually removed saved object references or if there are migration issues and the saved object + // references are removed. Also keeping it we can detect and log out a warning if the reference between it and the saved_object reference + // array have changed between each other indicating the saved_object array is being mutated outside of this functionality + const paramsWithoutSavedObjectReferences = { ...params }; + + return { + references: returnReferences, + params: paramsWithoutSavedObjectReferences, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts new file mode 100644 index 000000000000..b85555454583 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './inject_references'; +export * from './extract_references'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts new file mode 100644 index 000000000000..fc35088da66f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectExceptionsReferences } from './inject_exceptions_list'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns empty array given an empty array for both "exceptionsList" and "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: [], + }) + ).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + injectExceptionsReferences({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + }); + + test('returns empty array given an empty array for "exceptionsList"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual([]); + }); + + test('returns exceptions list array given an empty array for "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [], + }) + ).toEqual(mockExceptionsList()); + }); + + test('returns parameters from the saved object if found', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockExceptionsList()); + }); + + test('does not log an error if it returns parameters from the saved object when found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual([{ ...mockExceptionsList()[0], id: '456' }]); + }); + + test('logs an error if found with a different saved object reference id', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 456 is not the same as the "saved object id": 123. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('returns exceptionItem if the saved object reference cannot match as a fall back', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(mockExceptionsList()); + }); + + test('logs an error if the saved object type could not be found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts new file mode 100644 index 000000000000..2e6559fbf18c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts @@ -0,0 +1,62 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { + getSavedObjectReferenceForExceptionsList, + logMissingSavedObjectError, + logWarningIfDifferentReferencesDetected, +} from './utils'; + +/** + * This injects any "exceptionsList" "id"'s from saved object reference and returns the "exceptionsList" using the saved object reference. If for + * some reason it is missing on saved object reference, we log an error about it and then take the last known good value from the "exceptionsList" + * + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to merge the saved object reference from. + * @param savedObjectReferences The saved object references which should contain an "exceptionsList" + * @returns The exceptionsList with the saved object reference replacing any value in the saved object's id. + */ +export const injectExceptionsReferences = ({ + logger, + exceptionsList, + savedObjectReferences, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams['exceptionsList'] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + return []; + } + return exceptionsList.map((exceptionItem, index) => { + const savedObjectReference = getSavedObjectReferenceForExceptionsList({ + logger, + index, + savedObjectReferences, + }); + if (savedObjectReference != null) { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: savedObjectReference.id, + savedObjectId: exceptionItem.id, + }); + const reference: RuleParams['exceptionsList'][0] = { + ...exceptionItem, + id: savedObjectReference.id, + }; + return reference; + } else { + logMissingSavedObjectError({ logger, exceptionItem }); + return exceptionItem; + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts new file mode 100644 index 000000000000..a80f19ae011d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectReferences } from './inject_references'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns parameters from a saved object if found', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + + const returnParams: Partial = { + note: 'some note', + exceptionsList: [{ ...mockExceptionsList()[0], id: '456' }], + }; + + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual(returnParams as RuleParams); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('It returns params with an added exceptionsList if the exceptionsList is missing due to migration bugs', () => { + const params: Partial = { + note: 'some note', + }; + const returnParams: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(returnParams as RuleParams); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts new file mode 100644 index 000000000000..dae5e3037b73 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts @@ -0,0 +1,50 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { injectExceptionsReferences } from './inject_exceptions_list'; + +/** + * Injects references and returns the saved object references. + * How to add a new injected references here: + * --- + * Add a new file for injection named: inject_.ts, example: inject_foo.ts + * Add a new function into that file named: inject, example: injectFooReferences(logger, params.foo) + * Add a new line below and spread the new parameter together like so: + * + * const foo = injectFooReferences(logger, params.foo, savedObjectReferences); + * const ruleParamsWithSavedObjectReferences: RuleParams = { + * ...params, + * foo, + * exceptionsList, + * }; + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @param savedObjectReferences The saved object references to merge with the rule params + * @returns The rule parameters with the saved object references. + */ +export const injectReferences = ({ + logger, + params, + savedObjectReferences, +}: { + logger: Logger; + params: RuleParams; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams => { + const exceptionsList = injectExceptionsReferences({ + logger, + exceptionsList: params.exceptionsList, + savedObjectReferences, + }); + const ruleParamsWithSavedObjectReferences: RuleParams = { + ...params, + exceptionsList, + }; + return ruleParamsWithSavedObjectReferences; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts new file mode 100644 index 000000000000..1423dff3b92f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * The name of the exceptions list we give it when we save the saved object references. This name will + * end up in the saved object as in this example: + * { + * "references" : [ + * { + * "name" : "param:exceptionsList_1", + * "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + * "type" : "exception-list" + * } + * ] + * } + */ +export const EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME = 'exceptionsList'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts new file mode 100644 index 000000000000..41dc2d9179d8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSavedObjectNamePattern } from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 0 })).toEqual('test_0'); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 1 })).toEqual('test_1'); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: -1 })).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: NaN })).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts new file mode 100644 index 000000000000..f4e33cf57fa2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Given a name and index this will return the pattern of "${name_${index}" + * @param name The name to suffix the string + * @param index The index to suffix the string + * @returns The pattern "${name_${index}" + */ +export const getSavedObjectNamePattern = ({ + name, + index, +}: { + name: string; + index: number; +}): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return `${name}_${index}`; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts new file mode 100644 index 000000000000..98c575e8835b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts @@ -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 { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectNamePatternForExceptionsList, +} from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePatternForExceptionsList(0)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0` + ); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePatternForExceptionsList(1)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1` + ); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(-1)).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(NaN)).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts new file mode 100644 index 000000000000..c50530941168 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts @@ -0,0 +1,23 @@ +/* + * 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 { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given an index this will return the pattern of "exceptionsList_${index}" + * @param index The index to suffix the string + * @returns The pattern of "exceptionsList_${index}" + * @throws TypeError if index is less than zero + */ +export const getSavedObjectNamePatternForExceptionsList = (index: number): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectNamePattern({ name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, index }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts new file mode 100644 index 000000000000..70f321eed030 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { getSavedObjectReference } from '.'; + +describe('get_saved_object_reference', () => { + type FuncReturn = ReturnType; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: 'test_0', + type: 'some-type', + }, + ]; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: 'test_1', + type: 'some-type', + }, + ]; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: 'test_1', + }; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts new file mode 100644 index 000000000000..fe3a7393bf37 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given a saved object name, and an index, this will return the specific named saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the name to get the value. + * @param logger The kibana injected logger + * @param name The name of the saved object reference we are getting from the array + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReference = ({ + logger, + name, + index, + savedObjectReferences, +}: { + logger: Logger; + name: string; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else if (index > savedObjectReferences.length) { + logger.error( + [ + 'Cannot get a saved object reference using an index which is larger than the saved object references. Index is:', + index, + ' which is larger than the savedObjectReferences:', + JSON.stringify(savedObjectReferences), + ].join('') + ); + } else { + return savedObjectReferences.find( + (reference) => reference.name === getSavedObjectNamePattern({ name, index }) + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts new file mode 100644 index 000000000000..9a16037ed7fd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectReferenceForExceptionsList, +} from '.'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; + +describe('get_saved_object_reference_for_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + }; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts new file mode 100644 index 000000000000..d1534cc2a06b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectReference } from './get_saved_object_reference'; + +/** + * Given an index and a saved object reference, this will return the specific "exceptionsList" saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the "exceptionsList" name to get the value. + * @param logger The kibana injected logger + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReferenceForExceptionsList = ({ + logger, + index, + savedObjectReferences, +}: { + logger: Logger; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectReference({ + logger, + name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + index, + savedObjectReferences, + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts new file mode 100644 index 000000000000..ca88dae364a4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './constants'; +export * from './get_saved_object_name_pattern_for_exception_list'; +export * from './get_saved_object_name_pattern'; +export * from './get_saved_object_reference_for_exceptions_list'; +export * from './get_saved_object_reference'; +export * from './log_missing_saved_object_error'; +export * from './log_warning_if_different_references_detected'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts new file mode 100644 index 000000000000..d0158be28566 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; + +import { logMissingSavedObjectError } from '.'; + +describe('log_missing_saved_object_error', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message', () => { + logMissingSavedObjectError({ + logger, + exceptionItem: { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts new file mode 100644 index 000000000000..8d448c3cd10c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts @@ -0,0 +1,31 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { RuleParams } from '../../../schemas/rule_schemas'; + +/** + * This will log a warning that we are missing an object reference. + * @param logger The kibana injected logger + * @param exceptionItem The exception item to log the warning out as + */ +export const logMissingSavedObjectError = ({ + logger, + exceptionItem, +}: { + logger: Logger; + exceptionItem: RuleParams['exceptionsList'][0]; +}): void => { + logger.error( + [ + 'The saved object references were not found for our exception list when we were expecting to find it. ', + 'Kibana migrations might not have run correctly or someone might have removed the saved object references manually. ', + 'Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: ', + JSON.stringify(exceptionItem), + ].join('') + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts new file mode 100644 index 000000000000..a27faa6356c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.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 { loggingSystemMock } from 'src/core/server/mocks'; + +import { logWarningIfDifferentReferencesDetected } from '.'; + +describe('log_warning_if_different_references_detected', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message if the two ids are different', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '456', + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('logs nothing if the two ids are the same', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '123', + }); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts new file mode 100644 index 000000000000..9f80ba6d8ce8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; + +/** + * This will log a warning that the saved object reference id and the saved object id are not the same if that is true. + * @param logger The kibana injected logger + * @param savedObjectReferenceId The saved object reference id from "references: [{ id: ...}]" + * @param savedObjectId The saved object id from a structure such as exceptions { exceptionsList: { "id": "..." } } + */ +export const logWarningIfDifferentReferencesDetected = ({ + logger, + savedObjectReferenceId, + savedObjectId, +}: { + logger: Logger; + savedObjectReferenceId: string; + savedObjectId: string; +}): void => { + if (savedObjectReferenceId !== savedObjectId) { + logger.error( + [ + 'The id of the "saved object reference id": ', + savedObjectReferenceId, + ' is not the same as the "saved object id": ', + savedObjectId, + '. Preferring and using the "saved object reference id" instead of the "saved object id"', + ].join('') + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 711db931e907..8bf0c986b9c2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -32,11 +32,13 @@ import { bulkCreateFactory } from './bulk_create_factory'; import { wrapHitsFactory } from './wrap_hits_factory'; import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { BuildReasonMessage } from './reason_formatters'; const buildRuleMessage = mockBuildRuleMessage; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; + let buildReasonMessage: BuildReasonMessage; let bulkCreate: BulkCreate; let wrapHits: WrapHits; let inputIndexPattern: string[] = []; @@ -48,6 +50,7 @@ describe('searchAfterAndBulkCreate', () => { let tuple: RuleRangeTuple; beforeEach(() => { jest.clearAllMocks(); + buildReasonMessage = jest.fn().mockResolvedValue('some alert reason message'); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; @@ -191,6 +194,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -295,6 +299,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -373,6 +378,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -432,6 +438,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -511,6 +518,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -566,6 +574,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -638,6 +647,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -712,6 +722,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -763,6 +774,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -810,6 +822,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -871,6 +884,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -997,6 +1011,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -1093,6 +1108,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 7b5b61577cf3..8037a9a20151 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -34,6 +34,7 @@ export const searchAfterAndBulkCreate = async ({ filter, pageSize, buildRuleMessage, + buildReasonMessage, enrichment = identity, bulkCreate, wrapHits, @@ -146,7 +147,7 @@ export const searchAfterAndBulkCreate = async ({ ); } const enrichedEvents = await enrichment(filteredEvents); - const wrappedDocs = wrapHits(enrichedEvents.hits.hits); + const wrappedDocs = wrapHits(enrichedEvents.hits.hits, buildReasonMessage); const { bulkCreateDuration: bulkDuration, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 7e467891e6d4..b242691577b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -69,6 +69,7 @@ import { wrapHitsFactory } from './wrap_hits_factory'; import { wrapSequencesFactory } from './wrap_sequences_factory'; import { ConfigType } from '../../../config'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; +import { injectReferences, extractReferences } from './saved_object_references'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; import { IRuleDataPluginService } from '../rule_execution_log/types'; @@ -96,6 +97,11 @@ export const signalRulesAlertType = ({ name: 'SIEM signal', actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', + useSavedObjectReferences: { + extractReferences: (params) => extractReferences({ logger, params }), + injectReferences: (params, savedObjectReferences) => + injectReferences({ logger, params, savedObjectReferences }), + }, validate: { params: { validate: (object: unknown): RuleParams => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index fb9881b519a1..312d75f7a10c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -9,6 +9,7 @@ import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; +import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import { CreateThreatSignalOptions } from './types'; import { SearchAfterAndBulkCreateReturnType } from '../types'; @@ -83,6 +84,7 @@ export const createThreatSignal = async ({ filter: esFilter, pageSize: searchAfterSize, buildRuleMessage, + buildReasonMessage: buildReasonMessageForThreatMatchAlert, enrichment: threatEnrichment, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index f56ed3a5e9eb..afb0353c4ba0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -24,6 +24,7 @@ import { getThresholdAggregationParts, getThresholdTermsHash, } from '../utils'; +import { buildReasonMessageForThresholdAlert } from '../reason_formatters'; import type { MultiAggBucket, SignalSource, @@ -248,5 +249,7 @@ export const bulkCreateThresholdSignals = async ( params.thresholdSignalHistory ); - return params.bulkCreate(params.wrapHits(ecsResults.hits.hits)); + return params.bulkCreate( + params.wrapHits(ecsResults.hits.hits, buildReasonMessageForThresholdAlert) + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 6cbe0d1a5270..89233cf2c824 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -35,6 +35,7 @@ import { RuleParams } from '../schemas/rule_schemas'; import { GenericBulkCreateResponse } from './bulk_create_factory'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; +import { BuildReasonMessage } from './reason_formatters'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -195,7 +196,7 @@ export const isAlertExecutor = ( obj: SignalRuleAlertTypeDefinition ): obj is AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext, @@ -206,7 +207,7 @@ export const isAlertExecutor = ( export type SignalRuleAlertTypeDefinition = AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext, @@ -238,6 +239,7 @@ export interface Signal { }; original_time?: string; original_event?: SearchTypes; + reason?: string; status: Status; threshold_result?: ThresholdResult; original_signal?: SearchTypes; @@ -286,9 +288,15 @@ export type BulkCreate = (docs: Array>) => Promise; -export type WrapHits = (hits: estypes.SearchHit[]) => SimpleHit[]; +export type WrapHits = ( + hits: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; -export type WrapSequences = (sequences: Array>) => SimpleHit[]; +export type WrapSequences = ( + sequences: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; export interface SearchAfterAndBulkCreateParams { tuple: { @@ -308,6 +316,7 @@ export interface SearchAfterAndBulkCreateParams { pageSize: number; filter: unknown; buildRuleMessage: BuildRuleMessage; + buildReasonMessage: BuildReasonMessage; enrichment?: SignalsEnrichment; bulkCreate: BulkCreate; wrapHits: WrapHits; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts index 5cef740e1789..19bdd58140a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ { _index: signalsIndex, @@ -34,7 +34,7 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy), + _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy, buildReasonMessage), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts index f0b9e6404769..0ca4b9688f97 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts @@ -17,11 +17,17 @@ export const wrapSequencesFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapSequences => (sequences) => +}): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( (acc: WrappedSignalHit[], sequence) => [ ...acc, - ...buildSignalGroupFromSequence(sequence, ruleSO, signalsIndex, mergeStrategy), + ...buildSignalGroupFromSequence( + sequence, + ruleSO, + signalsIndex, + mergeStrategy, + buildReasonMessage + ), ], [] ); diff --git a/x-pack/plugins/security_solution/server/utils/serialized_query.ts b/x-pack/plugins/security_solution/server/utils/serialized_query.ts index 7f8603ccab4b..0c53de4b37f8 100644 --- a/x-pack/plugins/security_solution/server/utils/serialized_query.ts +++ b/x-pack/plugins/security_solution/server/utils/serialized_query.ts @@ -7,7 +7,7 @@ import { isEmpty, isPlainObject, isString } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export const parseFilterQuery = (filterQuery: string): JsonObject => { try { diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 0df41b9f988b..5c87d58199df 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -1,47 +1,46 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "scripts/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - "public/**/*.json", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../../../src/plugins/inspector/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../actions/tsconfig.json" }, - { "path": "../alerting/tsconfig.json" }, - { "path": "../cases/tsconfig.json" }, - { "path": "../data_enhanced/tsconfig.json" }, - { "path": "../encrypted_saved_objects/tsconfig.json" }, - { "path": "../features/tsconfig.json" }, - { "path": "../fleet/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../lists/tsconfig.json" }, - { "path": "../maps/tsconfig.json" }, - { "path": "../ml/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json"}, - { "path": "../timelines/tsconfig.json"}, - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "scripts/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry_management_section/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../fleet/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../lists/tsconfig.json" }, + { "path": "../maps/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json"}, + { "path": "../timelines/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/snapshot_restore/tsconfig.json b/x-pack/plugins/snapshot_restore/tsconfig.json index 39beda02977e..82f0e86df368 100644 --- a/x-pack/plugins/snapshot_restore/tsconfig.json +++ b/x-pack/plugins/snapshot_restore/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 3888519555d2..4cc95504a158 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index c83935945c67..f3ae4509f35b 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 14d95e3fd222..e237f5592419 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -17,7 +17,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -42,17 +41,6 @@ describe('config validation', () => { `); }); - test('the ElastiSearch Tasks index cannot be used for task manager', () => { - const config: Record = { - index: '.tasks', - }; - expect(() => { - configSchema.validate(config); - }).toThrowErrorMatchingInlineSnapshot( - `"[index]: \\".tasks\\" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager"` - ); - }); - test('the required freshness of the monitored stats config must always be less-than-equal to the poll interval', () => { const config: Record = { monitored_stats_required_freshness: 100, @@ -73,7 +61,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -116,7 +103,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 9b4f4856bf8a..7c541cd24cef 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -65,15 +65,6 @@ export const configSchema = schema.object( defaultValue: 1000, min: 1, }), - /* The name of the index used to store task information. */ - index: schema.string({ - defaultValue: '.kibana_task_manager', - validate: (val) => { - if (val.toLowerCase() === '.tasks') { - return `"${val}" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager`; - } - }, - }), /* The maximum number of tasks that this Kibana instance will run simultaneously. */ max_workers: schema.number({ defaultValue: DEFAULT_MAX_WORKERS, diff --git a/x-pack/plugins/task_manager/server/constants.ts b/x-pack/plugins/task_manager/server/constants.ts new file mode 100644 index 000000000000..9334fbede317 --- /dev/null +++ b/x-pack/plugins/task_manager/server/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 TASK_MANAGER_INDEX = '.kibana_task_manager'; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 182e7cd5bcab..859f242f2f0a 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -40,7 +40,6 @@ describe('EphemeralTaskLifecycle', () => { config: { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 8eb98c39a2cc..74d86c31e1bd 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -31,17 +31,6 @@ const applyTaskManagerDeprecations = (settings: Record = {}) => }; describe('deprecations', () => { - ['.foo', '.kibana_task_manager'].forEach((index) => { - it('logs a warning if index is set', () => { - const { messages } = applyTaskManagerDeprecations({ index }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.task_manager.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details", - ] - `); - }); - }); - it('logs a warning if max_workers is over limit', () => { const { messages } = applyTaskManagerDeprecations({ max_workers: 1000 }); expect(messages).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index cc4217f41c5e..067082955b3b 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -41,18 +41,6 @@ export const config: PluginConfigDescriptor = { deprecations: () => [ (settings, fromPath, addDeprecation) => { const taskManager = get(settings, fromPath); - if (taskManager?.index) { - addDeprecation({ - documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', - message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, - correctiveActions: { - manualSteps: [ - `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, - `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, - ], - }, - }); - } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 496c0138cb1e..ce49466ff387 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -31,7 +31,6 @@ describe('managed configuration', () => { const context = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts index 7a6bc5986210..d0e99690066d 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts @@ -6,7 +6,7 @@ */ import { isString } from 'lodash'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { HealthStatus, RawMonitoringStats } from '../monitoring'; import { TaskManagerConfig } from '../config'; diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts index 03efcff10eb6..9cc223f63b19 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -7,7 +7,7 @@ import { mapValues } from 'lodash'; import stats from 'stats-lite'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { RawMonitoringStats, RawMonitoredStat, HealthStatus } from './monitoring_stats_stream'; import { AveragedStat } from './task_run_calcultors'; import { TaskPersistenceTypes } from './task_run_statistics'; diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 82a111305927..e63beee7201f 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -15,7 +15,6 @@ describe('Configuration Statistics Aggregator', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts index d1f3ef9c1405..2378c7f4606a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts @@ -6,7 +6,7 @@ */ import { map, filter, startWith, buffer, share } from 'rxjs/operators'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { combineLatest, Observable, zip } from 'rxjs'; import { isOk, Ok } from '../lib/result_type'; import { AggregatedStat, AggregatedStatProvider } from './runtime_statistics_aggregator'; diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 8e615fb86171..d59d44614463 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -9,7 +9,7 @@ import { TaskManagerConfig } from '../config'; import { of, Subject } from 'rxjs'; import { take, bufferCount } from 'rxjs/operators'; import { createMonitoringStatsStream, AggregatedStat } from './monitoring_stats_stream'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; beforeEach(() => { jest.resetAllMocks(); @@ -19,7 +19,6 @@ describe('createMonitoringStatsStream', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index b187faf9e964..fdddfc41e590 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -9,7 +9,7 @@ import { merge, of, Observable } from 'rxjs'; import { map, scan } from 'rxjs/operators'; import { set } from '@elastic/safer-lodash-set'; import { Logger } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { TaskStore } from '../task_store'; import { TaskPollingLifecycle } from '../polling_lifecycle'; import { diff --git a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts index 799ea054596c..872da8e0cbd5 100644 --- a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts +++ b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; export interface AggregatedStat { key: string; diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts index b0611437d87b..f65c28562d2b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts @@ -6,7 +6,7 @@ */ import stats from 'stats-lite'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { isUndefined, countBy, mapValues } from 'lodash'; export interface AveragedStat extends JsonObject { diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index d43137d237a9..3946827827fe 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -7,7 +7,7 @@ import { combineLatest, Observable } from 'rxjs'; import { filter, startWith, map } from 'rxjs/operators'; -import { JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonObject, JsonValue } from '@kbn/utility-types'; import { isNumber, mapValues } from 'lodash'; import { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; import { TaskLifecycleEvent } from '../polling_lifecycle'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 5c4e7d6cbe2c..6f70df4b8c5c 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -8,7 +8,7 @@ import { combineLatest, Observable, timer } from 'rxjs'; import { mergeMap, map, filter, switchMap, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { keyBy, mapValues } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index dff94259dbe6..de21b653823c 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -18,7 +18,6 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, @@ -58,7 +57,6 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 3d3d180fc066..c41bc8109ef4 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -31,6 +31,7 @@ import { createMonitoringStats, MonitoringStats } from './monitoring'; import { EphemeralTaskLifecycle } from './ephemeral_task_lifecycle'; import { EphemeralTask } from './task'; import { registerTaskManagerUsageCollector } from './usage'; +import { TASK_MANAGER_INDEX } from './constants'; export type TaskManagerSetupContract = { /** @@ -114,7 +115,7 @@ export class TaskManagerPlugin } return { - index: this.config.index, + index: TASK_MANAGER_INDEX, addMiddleware: (middleware: Middleware) => { this.assertStillInSetup('add Middleware'); this.middleware = addMiddlewareToChain(this.middleware, middleware); @@ -134,7 +135,7 @@ export class TaskManagerPlugin serializer, savedObjectsRepository, esClient: elasticsearch.createClient('taskManager').asInternalUser, - index: this.config!.index, + index: TASK_MANAGER_INDEX, definitions: this.definitions, taskManagerId: `kibana:${this.taskManagerId!}`, }); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index aad03951bbb9..1420a81b2dca 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -38,7 +38,6 @@ describe('TaskPollingLifecycle', () => { config: { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index d2d079c7747b..e98a02b220d5 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -11,6 +11,7 @@ import mappings from './mappings.json'; import { migrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task'; +import { TASK_MANAGER_INDEX } from '../constants'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, @@ -23,11 +24,11 @@ export function setupSavedObjects( convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`, mappings: mappings.task as SavedObjectsTypeMappingDefinition, migrations, - indexPattern: config.index, + indexPattern: TASK_MANAGER_INDEX, excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestNeededActionParams = await getOldestIdleActionTask( readonlyEsClient, - config.index + TASK_MANAGER_INDEX ); // Delete all action tasks that have failed and are no longer needed diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 4b53dcac72c8..42ebd42b4f7a 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index f4c17c4317a9..03ca7efad22a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts index 292822019fc9..239e295a1f8b 100644 --- a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts @@ -292,6 +292,7 @@ export const systemFieldsMap: Readonly> = { export const signalFieldsMap: Readonly> = { 'signal.original_time': 'signal.original_time', + 'signal.reason': 'signal.reason', 'signal.rule.id': 'signal.rule.id', 'signal.rule.saved_id': 'signal.rule.saved_id', 'signal.rule.timeline_id': 'signal.rule.timeline_id', diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts index 4f4d21484267..c585d93330b2 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import type { Ecs } from '../../../../ecs'; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts index 7a344a163eba..5bceb3108168 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Inspect, Maybe } from '../../../common'; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts index 99ee021cb680..269fc6598bea 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; diff --git a/x-pack/plugins/timelines/common/typed_json.ts b/x-pack/plugins/timelines/common/typed_json.ts index c639c1c0322d..679a68a16f70 100644 --- a/x-pack/plugins/timelines/common/typed_json.ts +++ b/x-pack/plugins/timelines/common/typed_json.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { DslQuery, Filter } from '@kbn/es-query'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index fe9c5ea2bc33..2b58487fce53 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -14,8 +14,12 @@ import { mapSortDirectionToDirection, mapSortingColumns, stringifyEvent, + addBuildingBlockStyle, } from './helpers'; +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; +import { mockDnsEvent } from '../../../mock'; + describe('helpers', () => { describe('stringifyEvent', () => { test('it omits __typename when it appears at arbitrary levels', () => { @@ -388,4 +392,32 @@ describe('helpers', () => { ).toBe(false); }); }); + + describe('addBuildingBlockStyle', () => { + const THEME = { eui: euiThemeVars, darkMode: false }; + + test('it calls `setCellProps` with background color when event is a building block', () => { + const mockedSetCellProps = jest.fn(); + const ecs = { + ...mockDnsEvent, + ...{ signal: { rule: { building_block_type: ['default'] } } }, + }; + + addBuildingBlockStyle(ecs, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ + style: { + backgroundColor: euiThemeVars.euiColorHighlight, + }, + }); + }); + + test('it call `setCellProps` reseting the background color when event is not a building block', () => { + const mockedSetCellProps = jest.fn(); + + addBuildingBlockStyle(mockDnsEvent, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } }); + }); + }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index fb50d5ebabb8..3dea3e71445a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -7,6 +7,7 @@ import { isEmpty } from 'lodash/fp'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; import type { Ecs } from '../../../../common/ecs'; import type { BrowserField, @@ -20,6 +21,8 @@ import type { TimelineEventsType, } from '../../../../common/types/timeline'; +import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => k !== '__typename' && v != null ? v : undefined; @@ -144,6 +147,7 @@ export const allowSorting = ({ 'signal.parent.index', 'signal.parent.rule', 'signal.parent.type', + 'signal.reason', 'signal.rule.created_by', 'signal.rule.description', 'signal.rule.enabled', @@ -185,3 +189,23 @@ export const allowSorting = ({ return isAllowlistedNonBrowserField || isAggregatable; }; +export const addBuildingBlockStyle = ( + ecs: Ecs, + theme: EuiTheme, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] +) => { + if (isEventBuildingBlockType(ecs)) { + setCellProps({ + style: { + backgroundColor: `${theme.eui.euiColorHighlight}`, + }, + }); + } else { + // reset cell style + setCellProps({ + style: { + backgroundColor: 'inherit', + }, + }); + } +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 00f9d513a1a1..cc94f901446a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -24,28 +24,34 @@ import React, { useEffect, useMemo, useState, + useContext, } from 'react'; + import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { ThemeContext } from 'styled-components'; import { TGridCellAction, - TimelineId, - TimelineTabs, BulkActionsProp, - SortColumnTimeline, -} from '../../../../common/types/timeline'; - -import type { CellValueElementProps, ColumnHeaderOptions, ControlColumnProps, RowRenderer, AlertStatus, + SortColumnTimeline, + TimelineId, + TimelineTabs, } from '../../../../common/types/timeline'; + import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; -import { getEventIdToDataMapping, mapSortDirectionToDirection, mapSortingColumns } from './helpers'; +import { + addBuildingBlockStyle, + getEventIdToDataMapping, + mapSortDirectionToDirection, + mapSortingColumns, +} from './helpers'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; @@ -58,6 +64,7 @@ import { RowAction } from './row_action'; import * as i18n from './translations'; import { AlertCount } from '../styles'; import { checkBoxControlColumn } from './control_columns'; +import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -121,6 +128,7 @@ const transformControlColumns = ({ onSelectPage, browserFields, sort, + theme, }: { actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; @@ -138,6 +146,7 @@ const transformControlColumns = ({ browserFields: BrowserFields; onSelectPage: OnSelectAll; sort: SortColumnTimeline[]; + theme: EuiTheme; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ @@ -173,29 +182,33 @@ const transformControlColumns = ({ isExpanded, rowIndex, setCellProps, - }: EuiDataGridCellValueElementProps) => ( - - ), + }: EuiDataGridCellValueElementProps) => { + addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps); + + return ( + + ); + }, width: width ?? actionColumnsWidth, }) ); @@ -252,6 +265,7 @@ export const BodyComponent = React.memo( const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); + const theme: EuiTheme = useContext(ThemeContext); const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { setSelected({ @@ -444,6 +458,7 @@ export const BodyComponent = React.memo( sort, browserFields, onSelectPage, + theme, }) ); }, [ @@ -463,6 +478,7 @@ export const BodyComponent = React.memo( browserFields, onSelectPage, sort, + theme, ]); const columnsWithCellActions: EuiDataGridColumn[] = useMemo( @@ -483,34 +499,37 @@ export const BodyComponent = React.memo( [browserFields, columnHeaders, data, defaultCellActions] ); - const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({ - columnId, - rowIndex, - setCellProps, - }) => { - const rowData = rowIndex < data.length ? data[rowIndex].data : null; - const header = columnHeaders.find((h) => h.id === columnId); - const eventId = rowIndex < data.length ? data[rowIndex]._id : null; - - if (rowData == null || header == null || eventId == null) { - return null; - } - - return renderCellValue({ - columnId: header.id, - eventId, - data: rowData, - header, - isDraggable: false, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), - rowIndex, - setCellProps, - timelineId: tabType != null ? `${id}-${tabType}` : id, - }); - }; + const renderTGridCellValue: ( + x: EuiDataGridCellValueElementProps + ) => React.ReactNode = useCallback( + ({ columnId, rowIndex, setCellProps }) => { + const rowData = rowIndex < data.length ? data[rowIndex].data : null; + const header = columnHeaders.find((h) => h.id === columnId); + const eventId = rowIndex < data.length ? data[rowIndex]._id : null; + + addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps); + + if (rowData == null || header == null || eventId == null) { + return null; + } + + return renderCellValue({ + columnId: header.id, + eventId, + data: rowData, + header, + isDraggable: false, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), + rowIndex, + setCellProps, + timelineId: tabType != null ? `${id}-${tabType}` : id, + }); + }, + [columnHeaders, data, id, renderCellValue, tabType, theme] + ); return ( void; start: string; sort: Sort[]; - utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode; + additionalFilters: React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; leadingControlColumns?: ControlColumnProps[]; @@ -167,7 +169,7 @@ const TGridIntegratedComponent: React.FC = ({ setGlobalFullScreen, start, sort, - utilityBar, + additionalFilters, graphEventId, leadingControlColumns, trailingControlColumns, @@ -239,7 +241,7 @@ const TGridIntegratedComponent: React.FC = ({ const [ loading, - { events, updatedAt, loadPage, pageInfo, refetch, totalCount = 0, inspect }, + { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }, ] = useTimelineEvents({ alertConsumers: SECURITY_ALERTS_CONSUMERS, docValueFields, @@ -294,19 +296,17 @@ const TGridIntegratedComponent: React.FC = ({ height={ headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT } - subtitle={utilityBar} title={globalFullScreen ? titleWithExitFullScreen : justTitle} > {HeaderSectionContent} - - + {!resolverIsShowing(graphEventId) && additionalFilters} diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index df8a5897bfcd..7382d029be98 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index 391b15e8fdba..fe3d8c4bf2e2 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -6,7 +6,7 @@ */ import { isEmpty, isString, flow } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, Filter, Query } from '@kbn/es-query'; import { esQuery, esKuery, IIndexPattern } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx index 5fb0ed56afaa..b7345e90fdb0 100644 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import deepEqual from 'fast-deep-equal'; import { isEmpty, isString, noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7576c831554c..8e18281e7882 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { UpdateDocumentByQueryResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; + import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AlertStatus } from '../../../timelines/common'; @@ -24,7 +25,7 @@ export const useUpdateAlertsStatus = (): { updateAlertStatus: (params: { query: object; status: AlertStatus; - }) => Promise; + }) => Promise; } => { const { http } = useKibana().services; diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 335953e7ee43..426884ef8caa 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -57,11 +57,11 @@ export const useStatusBulkActionItems = ({ // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds, isDeleted: true }); - if (response.version_conflicts > 0 && eventIds.length === 1) { + if (response.version_conflicts && eventIds.length === 1) { throw new Error(i18n.BULK_ACTION_FAILED_SINGLE_ALERT); } - onUpdateSuccess(response.updated, response.version_conflicts, status); + onUpdateSuccess(response.updated ?? 0, response.version_conflicts ?? 0, status); } catch (error) { onUpdateFailure(status, error); } finally { @@ -83,21 +83,33 @@ export const useStatusBulkActionItems = ({ const actionItems = []; if (currentStatus !== FILTER_OPEN) { actionItems.push( - onClickUpdate(FILTER_OPEN)}> + onClickUpdate(FILTER_OPEN)} + > {i18n.BULK_ACTION_OPEN_SELECTED} ); } if (currentStatus !== FILTER_IN_PROGRESS) { actionItems.push( - onClickUpdate(FILTER_IN_PROGRESS)}> + onClickUpdate(FILTER_IN_PROGRESS)} + > {i18n.BULK_ACTION_IN_PROGRESS_SELECTED} ); } if (currentStatus !== FILTER_CLOSED) { actionItems.push( - onClickUpdate(FILTER_CLOSED)}> + onClickUpdate(FILTER_CLOSED)} + > {i18n.BULK_ACTION_CLOSE_SELECTED} ); diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 6f4de1dd2559..d31e80de7d28 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -52,7 +52,7 @@ export { getTimelineIdFromColumnDroppableId, } from './components/drag_and_drop/helpers'; export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser'; - +export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 24bc99e59aaf..2ec35ef1a51f 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -27,6 +27,7 @@ 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 { getHoverActions } from './components/hover_actions'; + export class TimelinesPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private _store: Store | undefined; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts index aae68dbcf86d..9b45a5bebfc2 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts @@ -45,6 +45,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'signal.status', 'signal.group.id', 'signal.original_time', + 'signal.reason', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 0517dcfb6490..4297cd595e26 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { DocValueFields } from '../../../../../../common/search_strategy'; export const buildTimelineDetailsQuery = ( diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index dfba32f8a238..a2a5a3bc8a49 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -5,14 +5,20 @@ * 2.0. */ -import { ALERT_OWNER, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; +import { ALERT_RULE_CONSUMER, ALERT_RULE_TYPE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { from } from 'rxjs'; -import { - isValidFeatureId, - mapConsumerToIndexName, + +import type { AlertConsumers, -} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; + mapConsumerToIndexName as mapConsumerToIndexNameTyped, + isValidFeatureId as isValidFeatureIdTyped, +} from '@kbn/rule-data-utils'; +import { + mapConsumerToIndexName as mapConsumerToIndexNameNonTyped, + isValidFeatureId as isValidFeatureIdNonTyped, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { AlertingAuthorizationEntity, @@ -38,6 +44,9 @@ import { ISearchOptions, } from '../../../../../../src/plugins/data/common'; +const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerToIndexNameNonTyped; +const isValidFeatureId: typeof isValidFeatureIdTyped = isValidFeatureIdNonTyped; + export const timelineSearchStrategyProvider = ( data: PluginStart, alerting: AlertingPluginStartContract @@ -129,7 +138,7 @@ const timelineAlertsSearchStrategy = ({ }) => { // Based on what solution alerts you want to see, figures out what corresponding // index to query (ex: siem --> .alerts-security.alerts) - const indices = alertConsumers.flatMap((consumer) => mapConsumerToIndexName[consumer]); + const indices = alertConsumers.flatMap((consumer) => `${mapConsumerToIndexName[consumer]}*`); const requestWithAlertsIndices = { ...request, defaultIndex: indices, indexName: indices }; // Note: Alerts RBAC are built off of the alerting's authorization class, which @@ -140,8 +149,8 @@ const timelineAlertsSearchStrategy = ({ type: AlertingAuthorizationFilterType.ESDSL, // Not passing in values, these are the paths for these fields fieldNames: { - consumer: ALERT_OWNER, - ruleTypeId: RULE_ID, + consumer: ALERT_RULE_CONSUMER, + ruleTypeId: ALERT_RULE_TYPE_ID, spaceIds: SPACE_IDS, }, }); diff --git a/x-pack/plugins/timelines/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json index 17a2a9b612ab..9677c0e64dd8 100644 --- a/x-pack/plugins/timelines/tsconfig.json +++ b/x-pack/plugins/timelines/tsconfig.json @@ -1,31 +1,30 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - "public/**/*.json", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../data_enhanced/tsconfig.json" }, - { "path": "../features/tsconfig.json" }, - { "path": "../cases/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../alerting/tsconfig.json" } - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 2717f92c7a4d..99e8baf3f92f 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8decd2a76b9b..9fbfe10f66b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1523,9 +1523,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "インデックスが存在することを確認してください。", "discover.docTable.limitedSearchResultLabel": "{resultCount}件の結果のみが表示されます。検索結果を絞り込みます。", "discover.docTable.noResultsTitle": "結果が見つかりませんでした", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表内の次ページ", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表内の前ページ", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "ドキュメント", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "{columnName}列を左に移動", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "列を左に移動", @@ -1993,7 +1990,6 @@ "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[e コマース] プロモーショントラッキング", "home.sampleData.ecommerceSpec.revenueDashboardDescription": "サンプルの e コマースの注文と収益を分析します", "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[e コマース] 収益ダッシュボード", - "home.sampleData.ecommerceSpec.salesCountMapTitle": "[eコマース] 売上カウントマップ", "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[e コマース] 1 日の販売製品", "home.sampleData.ecommerceSpecDescription": "e コマースの注文をトラッキングするサンプルデータ、ビジュアライゼーション、ダッシュボードです。", "home.sampleData.ecommerceSpecTitle": "サンプル e コマース注文", @@ -6111,7 +6107,6 @@ "xpack.apm.transactionDurationLabel": "期間", "xpack.apm.transactionErrorRateAlert.name": "トランザクションエラー率しきい値", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "より大きい", - "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "95 パーセンタイル", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "99 パーセンタイル", "xpack.apm.transactions.latency.chart.averageLabel": "平均", @@ -9680,7 +9675,6 @@ "xpack.fleet.editPackagePolicy.failedNotificationTitle": "「{packagePolicyName}」の更新エラー", "xpack.fleet.editPackagePolicy.pageDescription": "統合設定を修正し、選択したエージェントポリシーに変更をデプロイします。", "xpack.fleet.editPackagePolicy.pageTitle": "統合の編集", - "xpack.fleet.editPackagePolicy.pageTitleWithPackageName": "{packageName}統合の編集", "xpack.fleet.editPackagePolicy.saveButton": "統合の保存", "xpack.fleet.editPackagePolicy.updatedNotificationMessage": "Fleetは'{agentPolicyName}'ポリシーで使用されているすべてのエージェントに更新をデプロイします", "xpack.fleet.editPackagePolicy.updatedNotificationTitle": "正常に「{packagePolicyName}」を更新しました", @@ -9911,7 +9905,6 @@ "xpack.fleet.policyDetails.ErrorGettingFullAgentPolicy": "エージェントポリシーの読み込みエラー", "xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle": "アクション", "xpack.fleet.policyDetails.packagePoliciesTable.deleteActionTitle": "統合の削除", - "xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle": "説明", "xpack.fleet.policyDetails.packagePoliciesTable.editActionTitle": "統合の編集", "xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle": "名前", "xpack.fleet.policyDetails.packagePoliciesTable.namespaceColumnTitle": "名前空間", @@ -18518,10 +18511,20 @@ "xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル", "xpack.observability.expView.operationType.average": "平均", "xpack.observability.expView.operationType.median": "中央", + "xpack.observability.expView.reportType.noDataType": "データ型を選択すると、系列の構築を開始します。", + "xpack.observability.expView.seriesBuilder.breakdown": "内訳", + "xpack.observability.expView.seriesBuilder.dataType": "データ型", + "xpack.observability.expView.seriesBuilder.definition": "定義", + "xpack.observability.expView.seriesBuilder.filters": "フィルター", + "xpack.observability.expView.seriesBuilder.report": "レポート", "xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプを選択すると、ビジュアライゼーションを定義します。", + "xpack.observability.expView.seriesEditor.actions": "アクション", + "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します", + "xpack.observability.expView.seriesEditor.breakdowns": "内訳", "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", "xpack.observability.expView.seriesEditor.filters": "フィルター", "xpack.observability.expView.seriesEditor.name": "名前", + "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", "xpack.observability.featureCatalogueDescription1": "インフラストラクチャメトリックを監視します。", @@ -18587,6 +18590,7 @@ "xpack.observability.overview.uptime.up": "アップ", "xpack.observability.overview.ux.appLink": "アプリで表示", "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", + "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.title": "リソース", @@ -18600,6 +18604,7 @@ "xpack.observability.section.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", + "xpack.observability.seriesEditor.edit": "系列を編集", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均", "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", @@ -20611,7 +20616,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atWindowsT1053Description": " (Windows) (T1053.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.authenticationPackageT1547Description": "認証パッケージ (T1547.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashHistoryT1552Description": "Bash 履歴 (T1552.003) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description": ".bash_profile および .bashrc (T1546.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bidirectionalCommunicationT1102Description": "双方向通信 (T1102.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.binaryPaddingT1027Description": "バイナリパディング (T1027.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bootkitT1542Description": "ブートキット (T1542.003) ", @@ -20747,7 +20751,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalProxyT1090Description": "内部プロキシ (T1090.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description": "無効なコード署名 (T1036.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description": "IP アドレス (T1590.005) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description": "JavaScript/JScript (T1059.007) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.junkDataT1001Description": "ジャンクデータ (T1001.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kerberoastingT1558Description": "Kerberoasting (T1558.003) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kernelModulesAndExtensionsT1547Description": "カーネルモジュールおよび拡張 (T1547.006) ", @@ -20758,7 +20761,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchDaemonT1543Description": "デーモンの起動 (T1543.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchdT1053Description": "Launchd (T1053.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lcLoadDylibAdditionT1546Description": "LC_LOAD_DYLIB 追加 (T1546.006) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description": "LD_PRELOAD (T1574.006) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description": "Linux および Mac ファイルおよびディレクトリアクセス権修正 (T1222.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description": "LLMNR/NBT-NSポイズニングおよび SMB リレー (T1557.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountsT1078Description": "ローカルアカウント (T1078.003) ", @@ -20834,7 +20836,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pubPrnT1216Description": "PubPrn (T1216.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.purchaseTechnicalDataT1597Description": "技術データの購入 (T1597.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pythonT1059Description": "Python (T1059.006) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description": "Rc.common (T1037.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description": "RDP ハイジャック (T1563.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reduceKeySpaceT1600Description": "キースペースの削減 (T1600.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reflectionAmplificationT1498Description": "リフレクション (アンプ) 攻撃 (T1498.002) ", @@ -21044,7 +21045,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hijackExecutionFlowDescription": "ハイジャック実行フロー (T1574) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "ハイパーバイザー (T1062) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.impairDefensesDescription": "防御の破損 (T1562) ", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "コンテナーイメージの挿入 (T1525) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "ホストでのインジケーター削除 (T1070) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "間接コマンド実行 (T1202) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ingressToolTransferDescription": "Ingress Tool Transfer (T1105) ", @@ -21419,8 +21419,6 @@ "xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "現在{hostName}は{isolated}です。このホストを{unisolate}しますか?", "xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "分離済み", "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "推奨のデフォルト値で統合が保存されます。後からこれを変更するには、エージェントポリシー内で Endpoint Security 統合を編集します。", - "xpack.securitySolution.endpoint.ingestToastMessage": "Fleetが設定中に失敗しました。", - "xpack.securitySolution.endpoint.ingestToastTitle": "アプリを初期化できませんでした", "xpack.securitySolution.endpoint.list.actionmenu": "開く", "xpack.securitySolution.endpoint.list.actions": "アクション", "xpack.securitySolution.endpoint.list.endpointsEnrolling": "エンドポイントを登録しています。進行状況を追跡するには、{agentsLink}してください。", @@ -21527,7 +21525,6 @@ "xpack.securitySolution.endpoint.policy.details.platinum": "プラチナ", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", "xpack.securitySolution.endpoint.policy.details.protections": "保護", - "xpack.securitySolution.endpoint.policy.details.protectionsEnabled": "{protectionName}保護は{mode, select, true {有効です} false {無効です}}", "xpack.securitySolution.endpoint.policy.details.ransomware": "ランサムウェア", "xpack.securitySolution.endpoint.policy.details.save": "保存", "xpack.securitySolution.endpoint.policy.details.settings": "設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90e5e37f3d7a..abedf54509ba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1532,9 +1532,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "请确保索引存在。", "discover.docTable.limitedSearchResultLabel": "仅限于 {resultCount} 个结果。优化您的搜索。", "discover.docTable.noResultsTitle": "找不到结果", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表中下一页", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表中上一页", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "文档", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "向左移动“{columnName}”列", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "向左移动列", @@ -2004,7 +2001,6 @@ "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[电子商务] 促销追踪", "home.sampleData.ecommerceSpec.revenueDashboardDescription": "分析模拟的电子商务订单和收入", "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[电子商务] 收入仪表板", - "home.sampleData.ecommerceSpec.salesCountMapTitle": "[电子商务] 销售计数地图", "home.sampleData.ecommerceSpecDescription": "用于追踪电子商务订单的样例数据、可视化和仪表板。", "home.sampleData.ecommerceSpecTitle": "样例电子商务订单", "home.sampleData.flightsSpec.airportConnectionsTitle": "[航班] 机场航线(将鼠标悬停在机场上)", @@ -6145,7 +6141,6 @@ "xpack.apm.transactionDurationLabel": "持续时间", "xpack.apm.transactionErrorRateAlert.name": "事务错误率阈值", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "高于", - "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "第 95 个百分位", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "第 99 个百分位", "xpack.apm.transactions.latency.chart.averageLabel": "平均值", @@ -9942,7 +9937,6 @@ "xpack.fleet.editPackagePolicy.failedNotificationTitle": "更新“{packagePolicyName}”时出错", "xpack.fleet.editPackagePolicy.pageDescription": "修改集成设置并将更改部署到选定代理策略。", "xpack.fleet.editPackagePolicy.pageTitle": "编辑集成", - "xpack.fleet.editPackagePolicy.pageTitleWithPackageName": "编辑 {packageName} 集成", "xpack.fleet.editPackagePolicy.saveButton": "保存集成", "xpack.fleet.editPackagePolicy.updatedNotificationMessage": "Fleet 会将更新部署到所有使用策略“{agentPolicyName}”的代理", "xpack.fleet.editPackagePolicy.updatedNotificationTitle": "已成功更新“{packagePolicyName}”", @@ -10173,7 +10167,6 @@ "xpack.fleet.policyDetails.ErrorGettingFullAgentPolicy": "加载代理策略时出错", "xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle": "操作", "xpack.fleet.policyDetails.packagePoliciesTable.deleteActionTitle": "删除集成", - "xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle": "描述", "xpack.fleet.policyDetails.packagePoliciesTable.editActionTitle": "编辑集成", "xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle": "名称", "xpack.fleet.policyDetails.packagePoliciesTable.namespaceColumnTitle": "命名空间", @@ -18934,10 +18927,20 @@ "xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位", "xpack.observability.expView.operationType.average": "平均值", "xpack.observability.expView.operationType.median": "中值", + "xpack.observability.expView.reportType.noDataType": "选择数据类型以开始构建序列。", + "xpack.observability.expView.seriesBuilder.breakdown": "分解", + "xpack.observability.expView.seriesBuilder.dataType": "数据类型", + "xpack.observability.expView.seriesBuilder.definition": "定义", + "xpack.observability.expView.seriesBuilder.filters": "筛选", + "xpack.observability.expView.seriesBuilder.report": "报告", "xpack.observability.expView.seriesBuilder.selectReportType": "选择报告类型以定义可视化。", + "xpack.observability.expView.seriesEditor.actions": "操作", + "xpack.observability.expView.seriesEditor.addFilter": "添加筛选", + "xpack.observability.expView.seriesEditor.breakdowns": "分解", "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", "xpack.observability.expView.seriesEditor.filters": "筛选", "xpack.observability.expView.seriesEditor.name": "名称", + "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", "xpack.observability.featureCatalogueDescription1": "监测基础架构指标。", @@ -19003,6 +19006,7 @@ "xpack.observability.overview.uptime.up": "运行", "xpack.observability.overview.ux.appLink": "在应用中查看", "xpack.observability.overview.ux.title": "用户体验", + "xpack.observability.reportTypeCol.nodata": "没有可用数据", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.title": "资源", @@ -19016,6 +19020,7 @@ "xpack.observability.section.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", + "xpack.observability.seriesEditor.edit": "编辑序列", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均值", "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", @@ -21072,7 +21077,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atWindowsT1053Description": "At (Windows) (T1053.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.authenticationPackageT1547Description": "Authentication Package (T1547.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashHistoryT1552Description": "Bash History (T1552.003)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description": ".bash_profile and .bashrc (T1546.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bidirectionalCommunicationT1102Description": "Bidirectional Communication (T1102.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.binaryPaddingT1027Description": "Binary Padding (T1027.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bootkitT1542Description": "Bootkit (T1542.003)", @@ -21208,7 +21212,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalProxyT1090Description": "Internal Proxy (T1090.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description": "Invalid Code Signature (T1036.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description": "IP Addresses (T1590.005)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description": "JavaScript/JScript (T1059.007)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.junkDataT1001Description": "Junk Data (T1001.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kerberoastingT1558Description": "Kerberoasting (T1558.003)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kernelModulesAndExtensionsT1547Description": "Kernel Modules and Extensions (T1547.006)", @@ -21219,7 +21222,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchDaemonT1543Description": "Launch Daemon (T1543.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchdT1053Description": "Launchd (T1053.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lcLoadDylibAdditionT1546Description": "LC_LOAD_DYLIB Addition (T1546.006)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description": "LD_PRELOAD (T1574.006)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description": "Linux and Mac File and Directory Permissions Modification (T1222.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description": "LLMNR/NBT-NS Poisoning and SMB Relay (T1557.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountsT1078Description": "Local Accounts (T1078.003)", @@ -21295,7 +21297,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pubPrnT1216Description": "PubPrn (T1216.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.purchaseTechnicalDataT1597Description": "Purchase Technical Data (T1597.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pythonT1059Description": "Python (T1059.006)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description": "Rc.common (T1037.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description": "RDP Hijacking (T1563.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reduceKeySpaceT1600Description": "Reduce Key Space (T1600.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reflectionAmplificationT1498Description": "Reflection Amplification (T1498.002)", @@ -21505,7 +21506,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hijackExecutionFlowDescription": "Hijack Execution Flow (T1574)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "Hypervisor (T1062)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.impairDefensesDescription": "Impair Defenses (T1562)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "Implant Container Image (T1525)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "Indicator Removal on Host (T1070)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "Indirect Command Execution (T1202)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ingressToolTransferDescription": "Ingress Tool Transfer (T1105)", @@ -21903,8 +21903,6 @@ "xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} 当前{isolated}。是否确定要{unisolate}此主机?", "xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "已隔离", "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "我们将使用建议的默认值保存您的集成。稍后,您可以通过在代理策略中编辑 Endpoint Security 集成对其进行更改。", - "xpack.securitySolution.endpoint.ingestToastMessage": "Fleet 在设置期间失败。", - "xpack.securitySolution.endpoint.ingestToastTitle": "应用无法初始化", "xpack.securitySolution.endpoint.list.actionmenu": "未结", "xpack.securitySolution.endpoint.list.actions": "操作", "xpack.securitySolution.endpoint.list.endpointsEnrolling": "正在注册终端。{agentsLink}以跟踪进度。", @@ -22013,7 +22011,6 @@ "xpack.securitySolution.endpoint.policy.details.platinum": "白金级", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", "xpack.securitySolution.endpoint.policy.details.protections": "防护", - "xpack.securitySolution.endpoint.policy.details.protectionsEnabled": "{protectionName}防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.ransomware": "勒索软件", "xpack.securitySolution.endpoint.policy.details.save": "保存", "xpack.securitySolution.endpoint.policy.details.settings": "设置", diff --git a/x-pack/plugins/translations/tsconfig.json b/x-pack/plugins/translations/tsconfig.json index e48512742ed6..6b09de638f3f 100644 --- a/x-pack/plugins/translations/tsconfig.json +++ b/x-pack/plugins/translations/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 857582fa7cda..15d87605495f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -81,7 +81,7 @@ describe('jira connector validation', () => { }, secrets: { errors: { - apiToken: ['API token or password is required'], + apiToken: ['API token is required'], email: [], }, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 5904eb05c31b..1b9f3e2caa6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -67,28 +67,28 @@ export const JIRA_REENTER_VALUES_LABEL = i18n.translate( export const JIRA_EMAIL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', { - defaultMessage: 'Username or email address', + defaultMessage: 'Email address', } ); export const JIRA_EMAIL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', { - defaultMessage: 'Username or email address is required', + defaultMessage: 'Email address is required', } ); export const JIRA_API_TOKEN_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', { - defaultMessage: 'API token or password', + defaultMessage: 'API token', } ); export const JIRA_API_TOKEN_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', { - defaultMessage: 'API token or password is required', + defaultMessage: 'API token is required', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 5a8a9000ea5b..958511128de0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; import { AlertsList } from './alerts_list'; -import { ValidationResult } from '../../../../types'; +import { AlertTypeModel, ValidationResult } from '../../../../types'; import { AlertExecutionStatusErrorReasons, ALERTS_FEATURE_ID, @@ -44,6 +44,12 @@ jest.mock('react-router-dom', () => ({ pathname: '/triggersActions/alerts/', }), })); +jest.mock('../../../lib/capabilities', () => ({ + hasAllPrivilege: jest.fn(() => true), + hasSaveAlertsCapability: jest.fn(() => true), + hasShowActionsCapability: jest.fn(() => true), + hasExecuteActionsCapability: jest.fn(() => true), +})); const { loadAlerts, loadAlertTypes } = jest.requireMock('../../../lib/alert_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -264,7 +270,7 @@ describe('alerts_list component with items', () => { }, ]; - async function setup() { + async function setup(editable: boolean = true) { loadAlerts.mockResolvedValue({ page: 1, perPage: 10000, @@ -284,7 +290,20 @@ describe('alerts_list component with items', () => { loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); + const ruleTypeMock: AlertTypeModel = { + id: 'test_alert_type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; @@ -408,6 +427,18 @@ describe('alerts_list component with items', () => { }) ); }); + + it('renders edit and delete buttons when user can manage rules', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + }); + + it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { + await setup(false); + expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeFalsy(); + }); }); describe('alerts_list component empty with show only capability', () => { @@ -536,11 +567,18 @@ describe('alerts_list with show only capability', () => { }); } + it('renders table of alerts with edit button disabled', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); + }); + it('renders table of alerts with delete button disabled', async () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); - // TODO: check delete button + expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 3625dc07a118..941d40010408 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -50,7 +50,7 @@ import { deleteAlerts, } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; -import { hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; @@ -60,7 +60,6 @@ import { ALERTS_FEATURE_ID, AlertExecutionStatusErrorReasons, } from '../../../../../../alerting/common'; -import { hasAllPrivilege } from '../../../lib/capabilities'; import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; @@ -143,6 +142,9 @@ export const AlertsList: React.FunctionComponent = () => { setCurrentRuleToEdit(ruleItem); }; + const isRuleTypeEditableInContext = (ruleTypeId: string) => + ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; + useEffect(() => { loadAlertsData(); }, [ @@ -462,11 +464,11 @@ export const AlertsList: React.FunctionComponent = () => { name: '', width: '10%', render(item: AlertTableItem) { - return ( + return item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) ? ( - + { { defaultMessage: 'Edit' } )} className="alertSidebarItem__action" + data-test-subj="editActionHoverButton" onClick={() => onRuleEdit(item)} iconType={'pencil'} aria-label={i18n.translate( @@ -482,7 +485,7 @@ export const AlertsList: React.FunctionComponent = () => { )} /> - + { { defaultMessage: 'Delete' } )} className="alertSidebarItem__action" + data-test-subj="deleteActionHoverButton" onClick={() => setAlertsToDelete([item.id])} iconType={'trash'} aria-label={i18n.translate( @@ -500,6 +504,7 @@ export const AlertsList: React.FunctionComponent = () => { + { /> - ); + ) : null; }, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx new file mode 100644 index 000000000000..5a06b03311cb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx @@ -0,0 +1,345 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as React from 'react'; + +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { CollapsedItemActions } from './collapsed_item_actions'; +import { act } from 'react-dom/test-utils'; +import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; +import { AlertTableItem, AlertTypeModel } from '../../../../types'; +import { useKibana } from '../../../../common/lib/kibana'; +jest.mock('../../../../common/lib/kibana'); + +const onAlertChanged = jest.fn(); +const onEditAlert = jest.fn(); +const setAlertsToDelete = jest.fn(); +const disableAlert = jest.fn(); +const enableAlert = jest.fn(); +const unmuteAlert = jest.fn(); +const muteAlert = jest.fn(); + +export const tick = (ms = 0) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +describe('CollapsedItemActions', () => { + async function setup(editable: boolean = true) { + const ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.has.mockReturnValue(true); + const alertTypeR: AlertTypeModel = { + id: 'my-alert-type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + ruleTypeRegistry.get.mockReturnValue(alertTypeR); + const useKibanaMock = useKibana as jest.Mocked; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + } + + const getPropsWithRule = (overrides = {}, editable = false) => { + const rule: AlertTableItem = { + id: '1', + enabled: true, + name: 'test rule', + tags: ['tag1'], + alertTypeId: 'test_rule_type', + consumer: 'alerts', + schedule: { interval: '5d' }, + actions: [ + { id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } }, + ], + params: { name: 'test rule type name' }, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: '1m', + notifyWhen: 'onActiveAlert', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + actionsCount: 1, + tagsText: 'tag1', + alertType: 'Test Alert Type', + isEditable: true, + enabledInLicense: true, + ...overrides, + }; + + return { + item: rule, + onAlertChanged, + onEditAlert, + setAlertsToDelete, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + }; + }; + + test('renders panel items as disabled', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect( + wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled + ).toBeTruthy(); + }); + + test('renders closed popover initially and opens on click with all actions enabled', async () => { + await setup(); + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="selectActionButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeFalsy(); + + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled + ).toBeFalsy(); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('handles case when rule is unmuted and enabled and mute is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="muteButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(muteAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is unmuted and enabled and disable is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(disableAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is muted and enabled and unmute is clicked', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="muteButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(unmuteAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is unmuted and disabled and enable is clicked', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(enableAlert).toHaveBeenCalled(); + }); + + test('handles case when edit rule is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="editAlert"]').simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(onEditAlert).toHaveBeenCalled(); + }); + + test('handles case when delete rule is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="deleteAlert"]').simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(setAlertsToDelete).toHaveBeenCalled(); + }); + + test('renders actions correctly when rule is disabled', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Enable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule is not editable', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect( + wrapper.find(`[data-test-subj="selectActionButton"] button`).prop('disabled') + ).toBeTruthy(); + }); + + test('renders actions correctly when rule is not enabled due to license', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule is muted', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Unmute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule type is not editable in this context', async () => { + await setup(false); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index b4bf4e786bca..e2870e809794 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -10,6 +10,7 @@ import { asyncScheduler } from 'rxjs'; import React, { useEffect, useState } from 'react'; import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; import { AlertTableItem } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -22,7 +23,7 @@ export type ComponentOpts = { onAlertChanged: () => void; setAlertsToDelete: React.Dispatch>; onEditAlert: (item: AlertTableItem) => void; -} & BulkOperationsComponentOpts; +} & Pick; export const CollapsedItemActions: React.FunctionComponent = ({ item, @@ -34,6 +35,8 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setAlertsToDelete, onEditAlert, }: ComponentOpts) => { + const { ruleTypeRegistry } = useKibana().services; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isDisabled, setIsDisabled] = useState(!item.enabled); const [isMuted, setIsMuted] = useState(item.muteAll); @@ -42,9 +45,14 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setIsMuted(item.muteAll); }, [item.enabled, item.muteAll]); + const isRuleTypeEditableInContext = ruleTypeRegistry.has(item.alertTypeId) + ? !ruleTypeRegistry.get(item.alertTypeId).requiresAppContext + : false; + const button = ( setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -112,7 +120,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ), }, { - disabled: !item.isEditable, + disabled: !item.isEditable || !isRuleTypeEditableInContext, 'data-test-subj': 'editAlert', onClick: () => { setIsPopoverOpen(!isPopoverOpen); @@ -148,7 +156,12 @@ export const CollapsedItemActions: React.FunctionComponent = ({ panelPaddingSize="none" data-test-subj="collapsedItemActions" > - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx new file mode 100644 index 000000000000..bfa760b65ed4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { RuleEnabledSwitch, ComponentOpts } from './rule_enabled_switch'; + +describe('RuleEnabledSwitch', () => { + const enableAlert = jest.fn(); + const props: ComponentOpts = { + disableAlert: jest.fn(), + enableAlert, + item: { + id: '1', + name: 'test alert', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + consumer: 'test', + actionsCount: 0, + alertType: 'test_alert_type', + createdAt: new Date('2020-08-20T19:23:38Z'), + enabledInLicense: true, + isEditable: false, + notifyWhen: null, + tagsText: 'test', + updatedAt: new Date('2020-08-20T19:23:38Z'), + }, + onAlertChanged: jest.fn(), + }; + + beforeEach(() => jest.resetAllMocks()); + + test('renders switch control as disabled when rule is not editable', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="enableSwitch"]').first().props().disabled).toBeTruthy(); + }); + + test('renders switch control', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="enableSwitch"]').first().props().checked).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx index 49871549d273..21652f1cce78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -10,7 +10,7 @@ import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; import { Alert, AlertTableItem } from '../../../../types'; -interface ComponentOpts { +export interface ComponentOpts { item: AlertTableItem; onAlertChanged: () => void; enableAlert: (alert: Alert) => Promise; diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index 8202449b2229..6536206acf36 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ui_actions_enhanced/common/types.ts b/x-pack/plugins/ui_actions_enhanced/common/types.ts index f26134a23657..02cab5d17c0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/common/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/common/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; -export type BaseActionConfig = SerializableState; +export type BaseActionConfig = SerializableRecord; export type SerializedAction = { readonly factoryId: string; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts index 0e374010139f..5a34a002bf4c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts @@ -8,7 +8,7 @@ import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { PublicDrilldownManagerProps, DrilldownManagerDependencies, @@ -352,7 +352,7 @@ export class DrilldownManagerState { const action: SerializedAction = { factoryId: template.factoryId, name, - config: (template.config || {}) as SerializableState, + config: (template.config || {}) as SerializableRecord, }; await dynamicActionManager.createEvent(action, template.triggers); } @@ -395,7 +395,7 @@ export class DrilldownManagerState { if (drilldownState) { drilldownState.setName(this.pickName(template.name)); drilldownState.setTriggers(template.triggers); - drilldownState.setConfig(template.config as SerializableState); + drilldownState.setConfig(template.config as SerializableRecord); } }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts index 26134b7e17be..71ae3cfcc19e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts @@ -5,9 +5,9 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { EnhancementRegistryDefinition } from '../../../../../src/plugins/embeddable/public'; import { SavedObjectReference } from '../../../../../src/core/types'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; import { DynamicActionsState } from '../../../ui_actions_enhanced/public'; import { UiActionsServiceEnhancements } from '../services'; @@ -16,14 +16,14 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableState, telemetryData: Record) => { + telemetry: (state: SerializableRecord, telemetryData: Record) => { return uiActionsEnhanced.telemetry(state as DynamicActionsState, telemetryData); }, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return uiActionsEnhanced.extract(state as DynamicActionsState); }, - inject: (state: SerializableState, references: SavedObjectReference[]) => { + inject: (state: SerializableRecord, references: SavedObjectReference[]) => { return uiActionsEnhanced.inject(state as DynamicActionsState, references); }, - } as EnhancementRegistryDefinition; + } as EnhancementRegistryDefinition; }; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 88e4809a70fa..07cafef084a6 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector'; import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector'; @@ -17,14 +17,14 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (serializableState: SerializableState, stats: Record) => { + telemetry: (serializableState: SerializableRecord, stats: Record) => { const state = serializableState as DynamicActionsState; stats = dynamicActionsCollector(state, stats); stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); return stats; }, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { const references: SavedObjectReference[] = []; const newState: DynamicActionsState = { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { @@ -41,7 +41,7 @@ export const dynamicActionEnhancement = ( }; return { state: newState, references }; }, - inject: (state: SerializableState, references: SavedObjectReference[]) => { + inject: (state: SerializableRecord, references: SavedObjectReference[]) => { return { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { const factory = getActionFactory(event.action.factoryId); diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json index 39318770126e..100a1decd942 100644 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ b/x-pack/plugins/ui_actions_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 750bea75c665..33a1421fbb0c 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index aa981071b7ee..1a53a2c9b64a 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -22,7 +22,6 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import numeral from '@elastic/numeral'; import moment from 'moment'; -import { useSelector } from 'react-redux'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; import { UptimeThemeContext } from '../../../contexts'; @@ -33,7 +32,6 @@ import { getDateRangeFromChartElement } from './utils'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; -import { monitorStatusSelector } from '../../../state/selectors'; export interface PingHistogramComponentProps { /** @@ -75,8 +73,6 @@ export const PingHistogramComponent: React.FC = ({ const monitorId = useMonitorId(); - const selectedMonitor = useSelector(monitorStatusSelector); - const { basePath } = useUptimeSettingsContext(); const [getUrlParams, updateUrlParams] = useUrlParams(); @@ -193,21 +189,12 @@ export const PingHistogramComponent: React.FC = ({ const pingHistogramExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-pings`, - dataType: 'synthetics', - selectedMetricField: 'summary.up', - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.name': - monitorId && selectedMonitor?.monitor?.name - ? [selectedMonitor.monitor.name] - : ['ALL_VALUES'], - }, - }, - ], + 'pings-over-time': { + dataType: 'synthetics', + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), + }, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index c459fe46da97..9f00dd2e8f06 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -10,15 +10,13 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { createExploratoryViewUrl } from '../../../../../observability/public'; +import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; -import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { defaultMessage: 'Add data', @@ -40,28 +38,13 @@ export function ActionMenuContent(): React.ReactElement { const { dateRangeStart, dateRangeEnd } = params; const history = useHistory(); - const selectedMonitor = useSelector(monitorStatusSelector); - - const monitorId = selectedMonitor?.monitor?.id; - const syntheticExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'synthetics', - seriesType: 'area_stacked', - selectedMetricField: 'monitor.duration.us', - time: { from: dateRangeStart, to: dateRangeEnd }, - breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', - reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], - }, - name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', - }, - ], + 'synthetics-series': ({ + dataType: 'synthetics', + isNew: true, + time: { from: dateRangeStart, to: dateRangeEnd }, + } as unknown) as SeriesUrl, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 18aba948eaa3..1590e225f9ca 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -55,19 +55,16 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-response-duration`, - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], - }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', + [`monitor-duration`]: { + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.id': [monitorId] as string[], }, - ], + breakdown: 'observer.geo.name', + operationType: 'average', + dataType: 'synthetics', + }, }, basePath ); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index ce13ae4ce6ce..a51bbc49123d 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -17,7 +17,7 @@ import { ALERT_SEVERITY_VALUE, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, -} from '@kbn/rule-data-utils/target/technical_field_names'; +} from '@kbn/rule-data-utils'; interface MockAnomaly { severity: AnomaliesTableRecord['severity']; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 2388a789f3b8..100d992f5f86 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -13,7 +13,7 @@ import { ALERT_SEVERITY_VALUE, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, -} from '@kbn/rule-data-utils/target/technical_field_names'; +} from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { updateState, generateAlertMessage } from './common'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 249eaa33ec24..8fe11000725a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { min } from 'lodash'; import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index 15e6fe30db18..5227b8ca7dcc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters'; diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json index 88099b57f089..a41da4837f45 100644 --- a/x-pack/plugins/uptime/tsconfig.json +++ b/x-pack/plugins/uptime/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, @@ -32,4 +31,4 @@ "path": "../fleet/tsconfig.json" } ] -} \ No newline at end of file +} diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json index 15a28d498f2b..e17e7e753592 100644 --- a/x-pack/plugins/watcher/tsconfig.json +++ b/x-pack/plugins/watcher/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/xpack_legacy/tsconfig.json b/x-pack/plugins/xpack_legacy/tsconfig.json index 3bfc78b72cb3..57fccc031a0c 100644 --- a/x-pack/plugins/xpack_legacy/tsconfig.json +++ b/x-pack/plugins/xpack_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 275626664bef..6a6a0e13a1e1 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -115,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) { 'logs', 'maps', 'observabilityCases', + 'osquery', 'uptime', 'siem', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 2576a5eaf9bc..bbb0fc60cb3c 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -70,6 +70,19 @@ export default function ({ getService }: FtrProviderContext) { indexPatterns: ['all', 'read'], savedObjectsManagement: ['all', 'read'], timelion: ['all', 'read'], + osquery: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'live_queries_all', + 'live_queries_read', + 'run_saved_queries', + 'saved_queries_all', + 'saved_queries_read', + 'packs_all', + 'packs_read', + ], }, reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 25266da2cdfb..dc00be028412 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], + osquery: ['all', 'read'], ml: ['all', 'read'], siem: ['all', 'read'], fleet: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security_solution/events.ts b/x-pack/test/api_integration/apis/security_solution/events.ts index c99e41e011a9..f6a668679b11 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { Direction, diff --git a/x-pack/test/api_integration/apis/security_solution/utils.ts b/x-pack/test/api_integration/apis/security_solution/utils.ts index 9265a0066d20..c17a6abca27f 100644 --- a/x-pack/test/api_integration/apis/security_solution/utils.ts +++ b/x-pack/test/api_integration/apis/security_solution/utils.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonObject, JsonArray } from '@kbn/common-utils'; +import { JsonObject, JsonArray } from '@kbn/utility-types'; export const getFilterValue = (hostName: string, from: string, to: string): JsonObject => ({ bool: { diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 8072064b2b1b..0d19d3fd1cda 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -9,15 +9,11 @@ import expect from '@kbn/expect'; import { ALERT_DURATION, ALERT_END, - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_ID, - ALERT_OWNER, - ALERT_PRODUCER, ALERT_START, ALERT_STATUS, ALERT_UUID, EVENT_KIND, + ALERT_RULE_UUID, } from '@kbn/rule-data-utils'; import { merge, omit } from 'lodash'; import { format } from 'url'; @@ -219,7 +215,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { actions: [], tags: ['apm', 'service.name:opbeans-go'], notifyWhen: 'onActionGroupChange', - name: 'Transaction error rate threshold | opbeans-go', + name: 'Failed transaction rate threshold | opbeans-go', }; const { body: response, status } = await supertest @@ -350,7 +346,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { any >; - const exclude = ['@timestamp', ALERT_START, ALERT_UUID, 'rule.uuid']; + const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID]; const toCompare = omit(alertEvent, exclude); @@ -362,25 +358,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_DURATION}": Array [ + "kibana.alert.duration.us": Array [ 0, ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Failed transaction rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Failed transaction rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "open", ], "kibana.space_ids": Array [ @@ -389,15 +394,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], @@ -438,25 +434,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_DURATION}": Array [ + "kibana.alert.duration.us": Array [ 0, ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Failed transaction rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Failed transaction rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "open", ], "kibana.space_ids": Array [ @@ -465,15 +470,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], @@ -545,22 +541,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Failed transaction rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Failed transaction rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "closed", ], "kibana.space_ids": Array [ @@ -569,15 +574,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts index b767eaae1c20..805ec3adeace 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -368,7 +368,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0, - 15, + 3, ] `); }); @@ -397,7 +397,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0, - 187.5, + 37.5, ] `); }); diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap index a52084ffef43..a27f7047bb9b 100644 --- a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap @@ -1,135 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded has the correct throughput 1`] = ` -Array [ - Object { - "x": 1627973400000, - "y": 4, - }, - Object { - "x": 1627973460000, - "y": 2, - }, - Object { - "x": 1627973520000, - "y": 3, - }, - Object { - "x": 1627973580000, - "y": 8, - }, - Object { - "x": 1627973640000, - "y": 4, - }, - Object { - "x": 1627973700000, - "y": 4, - }, - Object { - "x": 1627973760000, - "y": 6, - }, - Object { - "x": 1627973820000, - "y": 10, - }, - Object { - "x": 1627973880000, - "y": 8, - }, - Object { - "x": 1627973940000, - "y": 9, - }, - Object { - "x": 1627974000000, - "y": 10, - }, - Object { - "x": 1627974060000, - "y": 11, - }, - Object { - "x": 1627974120000, - "y": 7, - }, - Object { - "x": 1627974180000, - "y": 10, - }, - Object { - "x": 1627974240000, - "y": 12, - }, - Object { - "x": 1627974300000, - "y": 6, - }, - Object { - "x": 1627974360000, - "y": 0, - }, - Object { - "x": 1627974420000, - "y": 4, - }, - Object { - "x": 1627974480000, - "y": 3, - }, - Object { - "x": 1627974540000, - "y": 5, - }, - Object { - "x": 1627974600000, - "y": 5, - }, - Object { - "x": 1627974660000, - "y": 5, - }, - Object { - "x": 1627974720000, - "y": 4, - }, - Object { - "x": 1627974780000, - "y": 7, - }, - Object { - "x": 1627974840000, - "y": 2, - }, - Object { - "x": 1627974900000, - "y": 14, - }, - Object { - "x": 1627974960000, - "y": 3, - }, - Object { - "x": 1627975020000, - "y": 6, - }, - Object { - "x": 1627975080000, - "y": 12, - }, - Object { - "x": 1627975140000, - "y": 8, - }, - Object { - "x": 1627975200000, - "y": 0, - }, -] -`; - -exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded with time comparison has the correct throughput 1`] = ` +exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded with time comparison has the correct throughput in tpm 1`] = ` Object { "currentPeriod": Array [ Object { @@ -263,5 +134,6 @@ Object { "y": 0, }, ], + "throughputUnit": "minute", } `; diff --git a/x-pack/test/apm_api_integration/tests/services/annotations.ts b/x-pack/test/apm_api_integration/tests/services/annotations.ts index 34eadbe3c609..a91fb59ce998 100644 --- a/x-pack/test/apm_api_integration/tests/services/annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/annotations.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { merge, cloneDeep, isPlainObject } from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts index c9c7d43762e7..2ecb9055baee 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -6,32 +6,39 @@ */ import expect from '@kbn/expect'; -import qs from 'querystring'; -import { first, last } from 'lodash'; +import { first, last, mean } from 'lodash'; import moment from 'moment'; import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; type ThroughputReturn = APIReturnType<'GET /api/apm/services/{serviceName}/throughput'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(getService('supertest')); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionType: 'request', - })}` - ); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + expect(response.status).to.be(200); expect(response.body.currentPeriod.length).to.be(0); expect(response.body.previousPeriod.length).to.be(0); @@ -43,46 +50,86 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Throughput when data is loaded', { config: 'basic', archives: [archiveName] }, () => { - before(async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionType: 'request', - })}` - ); - throughputResponse = response.body; + describe('when querying without kql filter', () => { + before(async () => { + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).not.to.be.greaterThan(0); + + const nonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) => + isFiniteNumber(y) + ); + + expect(nonNullDataPoints.length).to.be.greaterThan(0); + }); + + it('has the correct start date', () => { + expectSnapshot( + new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() + ).toMatchInline(`"2021-08-03T06:50:00.000Z"`); + }); + + it('has the correct end date', () => { + expectSnapshot( + new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() + ).toMatchInline(`"2021-08-03T07:20:00.000Z"`); + }); + + it('has the correct number of buckets', () => { + expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`31`); + }); + + it('has the correct throughput in tpm', () => { + const avg = mean(throughputResponse.currentPeriod.map((d) => d.y)); + expectSnapshot(avg).toMatchInline(`6.19354838709677`); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"minute"`); + }); }); - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).not.to.be.greaterThan(0); - - const nonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) => - isFiniteNumber(y) - ); - - expect(nonNullDataPoints.length).to.be.greaterThan(0); - }); - - it('has the correct start date', () => { - expectSnapshot( - new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() - ).toMatchInline(`"2021-08-03T06:50:00.000Z"`); - }); - - it('has the correct end date', () => { - expectSnapshot( - new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() - ).toMatchInline(`"2021-08-03T07:20:00.000Z"`); - }); - - it('has the correct number of buckets', () => { - expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`31`); - }); - - it('has the correct throughput', () => { - expectSnapshot(throughputResponse.currentPeriod).toMatch(); + describe('with kql filter to force transaction-based UI', () => { + before(async () => { + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + kuery: 'processor.event : "transaction"', + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + throughputResponse = response.body; + }); + + it('has the correct throughput in tps', async () => { + const avgTps = mean(throughputResponse.currentPeriod.map((d) => d.y)); + expectSnapshot(avgTps).toMatchInline(`0.124043715846995`); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"second"`); + + // this tpm value must be similar tp tpm value calculated in the previous spec where metric docs were used + const avgTpm = avgTps * 60; + expectSnapshot(avgTpm).toMatchInline(`7.44262295081967`); + }); }); } ); @@ -92,15 +139,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { before(async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - transactionType: 'request', - start: moment(metadata.end).subtract(15, 'minutes').toISOString(), - end: metadata.end, - comparisonStart: metadata.start, - comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(), - })}` - ); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + transactionType: 'request', + start: moment(metadata.end).subtract(15, 'minutes').toISOString(), + end: metadata.end, + comparisonStart: metadata.start, + comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(), + }, + }, + }); + throughputResponse = response.body; }); @@ -144,8 +198,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(throughputResponse.previousPeriod.length).toMatchInline(`16`); }); - it('has the correct throughput', () => { + it('has the correct throughput in tpm', () => { expectSnapshot(throughputResponse).toMatch(); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"minute"`); }); } ); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index a03bd07c8602..cd209da25e88 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -193,6 +193,7 @@ export default ({ getService }: FtrProviderContext) => { index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', depth: 0, }, + reason: `Alert Test ML rule created at ${signal._source['@timestamp']} with a critical severity and risk score of 50 by root on mothra.`, original_time: '2020-11-16T22:58:08.000Z', }, all_field_values: [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index c34176116063..399eafc475a8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -275,6 +275,7 @@ export default ({ getService }: FtrProviderContext) => { depth: 0, }, ], + reason: `Alert Query with a rule id created at ${fullSignal['@timestamp']} with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, status: 'open', }, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index dbffeacb03b7..48832cef27cd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -33,7 +33,8 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('Rule exception operators for data type text', () => { + // FLAKY: https://github.com/elastic/kibana/issues/107911 + describe.skip('Rule exception operators for data type text', () => { beforeEach(async () => { await createSignalsIndex(supertest); await createListsIndex(supertest); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 66c94a7317b7..1c1e2b9966b7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -112,7 +112,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -165,7 +166,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -228,7 +230,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -360,6 +362,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -494,6 +497,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -658,6 +662,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, group: fullSignal.signal.group, original_time: fullSignal.signal.original_time, @@ -748,6 +753,7 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', depth: 2, group: source.signal.group, + reason: `Alert Signal Testing Query created at ${source['@timestamp']} with a high severity and risk score of 1.`, rule: source.signal.rule, ancestors: [ { @@ -866,6 +872,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1003,6 +1010,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1086,6 +1094,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1171,7 +1180,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1228,7 +1238,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1325,7 +1335,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1387,7 +1398,7 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1675,6 +1686,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert boot created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on zeek-sensor-amsterdam.`, rule: { ...fullSignal.signal.rule, name: 'boot', diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index 073d08c5b1d8..8c6603a3e38b 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -14,6 +14,8 @@ const TEST_INDEX = 'logs-log.log-test'; const FINAL_PIPELINE_ID = '.fleet_final_pipeline-1'; +const FINAL_PIPELINE_VERSION = 1; + let pkgKey: string; export default function (providerContext: FtrProviderContext) { @@ -81,11 +83,32 @@ export default function (providerContext: FtrProviderContext) { } }); + it('should correctly update the final pipeline', async () => { + await es.ingest.putPipeline({ + id: FINAL_PIPELINE_ID, + body: { + description: 'Test PIPELINE WITHOUT version', + processors: [ + { + set: { + field: 'my-keyword-field', + value: 'foo', + }, + }, + ], + }, + }); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx'); + const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); + expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); + expect(pipelineRes.body[FINAL_PIPELINE_ID].version).to.be(1); + }); + it('should correctly setup the final pipeline and apply to fleet managed index template', async () => { const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); const res = await es.indices.getIndexTemplate({ name: 'logs-log.log' }); - expect(res.body.index_templates.length).to.be(1); + expect(res.body.index_templates.length).to.be(FINAL_PIPELINE_VERSION); expect(res.body.index_templates[0]?.index_template?.composed_of).to.contain( '.fleet_component_template-1' ); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 1cd94ba87ed5..b310c88b5854 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; import { UpgradePackagePolicyDryRunResponse, @@ -28,6 +29,8 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); + setupFleetAndAgents(providerContext); + describe('when package is installed', function () { before(async function () { await supertest @@ -223,7 +226,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('when "dryRun: true" is provided', function () { - it('should return a diff with errors', async function () { + it('should return a diff with missingVars', async function () { const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest .post(`/api/fleet/package_policies/upgrade`) .set('kbn-xsrf', 'xxxx') @@ -235,12 +238,13 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); - expect(body[0].hasErrors).to.be(true); + expect(body[0].hasErrors).to.be(false); + expect(body[0].diff?.[1].missingVars).to.contain('test_var'); }); }); describe('when "dryRun: false" is provided', function () { - it('should respond with an error', async function () { + it('should succeed', async function () { const { body }: { body: UpgradePackagePolicyResponse } = await supertest .post(`/api/fleet/package_policies/upgrade`) .set('kbn-xsrf', 'xxxx') @@ -251,7 +255,7 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(body.length).to.be(1); - expect(body[0].success).to.be(false); + expect(body[0].success).to.be(true); }); }); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c05b15905b93..3542abf9ea86 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -22,6 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const kibanaServer = getService('kibanaServer'); async function setDiscoverTimeRange() { await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -36,8 +37,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } ); await spacesService.create({ id: 'custom_space', @@ -48,8 +53,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); @@ -84,8 +93,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); await spacesService.create({ id: 'custom_space', @@ -96,8 +105,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); diff --git a/x-pack/test/functional/apps/observability/exploratory_view.ts b/x-pack/test/functional/apps/observability/exploratory_view.ts deleted file mode 100644 index 8f27f20ce30e..000000000000 --- a/x-pack/test/functional/apps/observability/exploratory_view.ts +++ /dev/null @@ -1,82 +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 Path from 'path'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['observability', 'common', 'header']); - const esArchiver = getService('esArchiver'); - const find = getService('find'); - - const testSubjects = getService('testSubjects'); - - const rangeFrom = '2021-01-17T16%3A46%3A15.338Z'; - const rangeTo = '2021-01-19T17%3A01%3A32.309Z'; - - // Failing: See https://github.com/elastic/kibana/issues/106934 - describe.skip('ExploratoryView', () => { - before(async () => { - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data') - ); - - await PageObjects.common.navigateToApp('ux', { - search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - }); - - it('should able to open exploratory view from ux app', async () => { - await testSubjects.exists('uxAnalyzeBtn'); - await testSubjects.click('uxAnalyzeBtn'); - expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true); - }); - - it('renders lens visualization', async () => { - expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true); - - expect( - await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]') - ).to.eql(true); - - expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true); - }); - - it('can do a breakdown per series', async () => { - await testSubjects.click('seriesBreakdown'); - - expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true); - - await find.clickByCssSelector('[id="user_agent.name"]'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - - expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true); - expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true); - }); - }); -} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts index cce07b9ff7d8..b7f03b5f27ba 100644 --- a/x-pack/test/functional/apps/observability/index.ts +++ b/x-pack/test/functional/apps/observability/index.ts @@ -8,9 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('ObservabilityApp', function () { + describe('Observability specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./exploratory_view')); }); } 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 eb2e339e9be6..dd5c1e63f1be 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -92,7 +92,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { "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.", + "status": "Completed at 2021-07-19 @ 10:29 PM See report info for warnings.", }, Object { "actions": "", @@ -104,37 +104,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { "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.", + "status": "Completed at 2021-07-19 @ 06:46 PM See 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.", + "status": "Completed at 2021-07-19 @ 06:44 PM See 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.", + "status": "Pending at 2021-07-19 @ 06:41 PM Waiting for job to process.", }, 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.", + "status": "Failed at 2021-07-19 @ 06:43 PM See 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.", + "status": "Completed at 2021-07-19 @ 06:41 PM See 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.", + "status": "Completed at 2021-07-19 @ 06:39 PM See report info for warnings.", }, Object { "actions": "", diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json deleted file mode 100644 index af5aa6d04348..000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:index-pattern:logstash-*", - "source": { - "namespace": "custom_space", - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:config:6.0.0", - "source": { - "namespace": "custom_space", - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json deleted file mode 100644 index 0cd1a29f9224..000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "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" - } - } - }, - "gis-map" : { - "properties" : { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description" : { - "type" : "text" - }, - "layerListJSON" : { - "type" : "text" - }, - "mapStateJSON" : { - "type" : "text" - }, - "title" : { - "type" : "text" - }, - "uiStateJSON" : { - "type" : "text" - }, - "version" : { - "type" : "integer" - } - } - }, - "graph-workspace": { - "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": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "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": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "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" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - } - } -} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index 940ebe5321b9..81ff00790336 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -6,9 +6,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -23,9 +23,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space1"] } @@ -40,9 +40,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space2"] } @@ -57,9 +57,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -74,9 +74,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.customRule", + "kibana.alert.rule.rule_type_id": "siem.customRule", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -90,9 +90,9 @@ "id": "space1securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1"] } @@ -106,9 +106,9 @@ "id": "space2securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space2"] } diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json index 74d50ca402e4..943457ad6cd8 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -13,7 +13,7 @@ } } }, - "kibana.alert.owner": { + "kibana.alert.rule.consumer": { "type": "keyword", "ignore_above": 256 } @@ -37,7 +37,7 @@ } } }, - "kibana.alert.owner": { + "kibana.alert.rule.consumer": { "type": "keyword", "ignore_above": 256 } diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json new file mode 100644 index 000000000000..d6c52e43984e --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzYsMl0=" +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json new file mode 100644 index 000000000000..9ddc185cf353 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json @@ -0,0 +1,32 @@ +{ + "attributes": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "7.15.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2021-08-04T13:18:45.677Z", + "version": "WzksMl0=" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzQsMl0=" +} diff --git a/x-pack/test/observability_api_integration/basic/tests/annotations.ts b/x-pack/test/observability_api_integration/basic/tests/annotations.ts index 4a2c7b68f612..6b1cdb43f5f5 100644 --- a/x-pack/test/observability_api_integration/basic/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/basic/tests/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/observability_api_integration/trial/tests/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts index b1ef717ddfd8..48b16b712bf3 100644 --- a/x-pack/test/observability_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { Annotation } from '../../../../plugins/observability/common/annotations'; import { FtrProviderContext } from '../../common/ftr_provider_context'; diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index 317010aca24b..bedaca0fc183 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { Router } from 'react-router-dom'; import React, { useCallback, useRef } from 'react'; import ReactDOM from 'react-dom'; @@ -15,6 +17,8 @@ import { KibanaContextProvider } from '../../../../../../../../src/plugins/kiban import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; +const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; + type CoreStartTimelines = CoreStart & { data: DataPublicPluginStart }; /** @@ -38,7 +42,7 @@ export function renderApp( ReactDOM.unmountComponentAtNode(parameters.element); }; } -const ALERT_CONSUMER = [AlertConsumers.SIEM]; +const ALERT_RULE_CONSUMER = [AlertConsumers.SIEM]; const AppRoot = React.memo( ({ @@ -63,7 +67,7 @@ const AppRoot = React.memo( {(timelinesPluginSetup && timelinesPluginSetup.getTGrid && timelinesPluginSetup.getTGrid<'standalone'>({ - alertConsumers: ALERT_CONSUMER, + alertConsumers: ALERT_RULE_CONSUMER, type: 'standalone', columns: [], indexNames: [], diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts index 47aafc400ce7..bfaeff7f366a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts @@ -85,9 +85,9 @@ export function resolveTestSuiteFactory(esArchiver: any, supertest: SuperTest { const expectedData = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], [ @@ -186,7 +186,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Policy Status', 'IP Address', 'Hostname', - 'Sensor Version', + 'Version', ]; const keys = await pageObjects.endpoint.endpointFlyoutDescriptionKeys( 'endpointDetailsFlyout' @@ -233,14 +233,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await querySubmitButton.click(); const expectedDataFromQuery = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], ['No items found'], @@ -263,14 +263,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await querySubmitButton.click(); const expectedDataFromQuery = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], [ diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index b3aeb55eb38a..81299e1cc7e2 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { eventIDSafeVersion, diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts index 79f5768b9a3b..67371338a925 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { User } from '../../../../rule_registry/common/lib/authentication/types'; import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -84,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { @@ -149,7 +149,9 @@ export default ({ getService }: FtrProviderContext) => { timeline.edges.every((hit: TimelineEdges) => { const data: TimelineNonEcsData[] = hit.node.data; return data.some(({ field, value }) => { - return field === ALERT_OWNER && featureIds.includes((value && value[0]) ?? ''); + return ( + field === ALERT_RULE_CONSUMER && featureIds.includes((value && value[0]) ?? '') + ); }); }) ).to.equal(true); diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts index 2ebb534c4a45..79fd9e6a4fb0 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { User } from '../../../../rule_registry/common/lib/authentication/types'; import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -67,7 +67,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { @@ -131,7 +131,9 @@ export default ({ getService }: FtrProviderContext) => { timeline.edges.every((hit: TimelineEdges) => { const data: TimelineNonEcsData[] = hit.node.data; return data.some(({ field, value }) => { - return field === ALERT_OWNER && featureIds.includes((value && value[0]) ?? ''); + return ( + field === ALERT_RULE_CONSUMER && featureIds.includes((value && value[0]) ?? '') + ); }); }) ).to.equal(true); diff --git a/x-pack/test/timeline/security_only/tests/basic/events.ts b/x-pack/test/timeline/security_only/tests/basic/events.ts index fc8ee95dedfc..e1ab3c47e111 100644 --- a/x-pack/test/timeline/security_only/tests/basic/events.ts +++ b/x-pack/test/timeline/security_only/tests/basic/events.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { JsonObject } from '@kbn/utility-types'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; @@ -40,7 +40,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { diff --git a/x-pack/test/timeline/security_only/tests/trial/events.ts b/x-pack/test/timeline/security_only/tests/trial/events.ts index fc8ee95dedfc..e1ab3c47e111 100644 --- a/x-pack/test/timeline/security_only/tests/trial/events.ts +++ b/x-pack/test/timeline/security_only/tests/trial/events.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { JsonObject } from '@kbn/utility-types'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; @@ -40,7 +40,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { diff --git a/x-pack/test/timeline/spaces_only/tests/events.ts b/x-pack/test/timeline/spaces_only/tests/events.ts index 829d46905b6d..3867279fda7f 100644 --- a/x-pack/test/timeline/spaces_only/tests/events.ts +++ b/x-pack/test/timeline/spaces_only/tests/events.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../rule_registry/common/ftr_provider_context'; import { getSpaceUrlPrefix } from '../../../rule_registry/common/lib/authentication/spaces'; @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cd43e7108b06..1bfd24d6ad29 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index afd8d1fb54cf..aeaaf7fca1cb 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'ml_file_data_visualizer' && catalogueId !== 'monitoring' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -74,6 +75,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'appSearch', 'workplaceSearch', 'spaces', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index b87a6475526f..6a6b618c2c8c 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'everything_space_all at everything_space': @@ -57,7 +57,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index d64b4f75e20a..da4b26106afa 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'monitoring' && catalogueId !== 'ml_file_data_visualizer' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -70,6 +71,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index 4d96532b83d4..6a44b3d8f0b7 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'read': @@ -55,7 +55,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break; diff --git a/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json b/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json index f1bf94a38de8..e625acbc569c 100644 --- a/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json +++ b/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "public/**/*.ts", diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json index f1bf94a38de8..e625acbc569c 100644 --- a/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "public/**/*.ts", diff --git a/yarn.lock b/yarn.lock index 7f42fc1902b1..5ff06955b63c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@33.2.0": - version "33.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.2.0.tgz#2135e86bfdde9796b64c8e58d29b0b193ddf47a9" - integrity sha512-Wk7IbfbxncAznTgMO5crRut7cS2GW8e8k++sg23epa6sT4RAnWwsKsvBojQdx9IvTWYxa+YJt4e7wCs3m5MSfw== +"@elastic/charts@33.2.2": + version "33.2.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.2.2.tgz#403c46eebe71f4ca7e5c9c1a135eec66869961cc" + integrity sha512-g+z1T8s6m7eySaxcY7R6yqUHUstUtEIH0P4FineKWdZ5L6IkxBNrhM7r0FaddIurNxvBy/SGQorhmFZAksWhiQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -2750,10 +2750,6 @@ version "0.0.0" uid "" -"@kbn/common-utils@link:bazel-bin/packages/kbn-common-utils": - version "0.0.0" - uid "" - "@kbn/config-schema@link:bazel-bin/packages/kbn-config-schema": version "0.0.0" uid ""