From 2fe4dcb3c27171d18aa1b47bea34981fa99e5257 Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Fri, 4 Mar 2022 07:32:13 -0800
Subject: [PATCH 01/27] [DOCS] Add license expiration for searchable snapshot
(#126865)
Co-authored-by: Leaf-Lin <39002973+Leaf-Lin@users.noreply.github.com>
---
docs/management/managing-licenses.asciidoc | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index 8944414f6bfbc..cf501518ea534 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -79,6 +79,7 @@ cluster.
* The deprecation API is disabled.
* SQL support is disabled.
* Aggregations provided by the analytics plugin are no longer usable.
+* All searchable snapshots indices are unassigned and cannot be searched.
[discrete]
[[expiration-watcher]]
From 79935f3341f4d234b79ea0c5f7f3de817134556b Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Fri, 4 Mar 2022 11:19:22 -0500
Subject: [PATCH 02/27] [CI] Expand spot instance trial a bit (#126928)
---
.buildkite/pipelines/hourly.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml
index e5bc841774fde..8c1162954cfac 100644
--- a/.buildkite/pipelines/hourly.yml
+++ b/.buildkite/pipelines/hourly.yml
@@ -19,7 +19,7 @@ steps:
label: 'Default CI Group'
parallelism: 27
agents:
- queue: n2-4
+ queue: n2-4-spot
depends_on: build
timeout_in_minutes: 250
key: default-cigroup
@@ -31,7 +31,7 @@ steps:
- command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh
label: 'Docker CI Group'
agents:
- queue: n2-4
+ queue: n2-4-spot
depends_on: build
timeout_in_minutes: 120
key: default-cigroup-docker
@@ -44,7 +44,7 @@ steps:
label: 'OSS CI Group'
parallelism: 11
agents:
- queue: ci-group-4d
+ queue: n2-4-spot
depends_on: build
timeout_in_minutes: 120
key: oss-cigroup
@@ -98,7 +98,7 @@ steps:
- command: .buildkite/scripts/steps/functional/oss_firefox.sh
label: 'OSS Firefox Tests'
agents:
- queue: ci-group-4d
+ queue: n2-4-spot
depends_on: build
timeout_in_minutes: 120
retry:
@@ -130,7 +130,7 @@ steps:
- command: .buildkite/scripts/steps/functional/oss_misc.sh
label: 'OSS Misc Functional Tests'
agents:
- queue: n2-4
+ queue: n2-4-spot
depends_on: build
timeout_in_minutes: 120
retry:
From 0b876fe350ff6f875bb8b3eb770d4f06c32ac0d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emilio=20Alvarez=20Pi=C3=B1eiro?=
<95703246+emilioalvap@users.noreply.github.com>
Date: Fri, 4 Mar 2022 17:51:07 +0100
Subject: [PATCH 03/27] Add Synthetics flaky test runner pipeline config
(#126602)
* Add flaky test runner pipeline config
* Add default value to grep expression if not set
* Add kibana build id override
* Add concurreny option (limited)
---
.buildkite/scripts/steps/functional/uptime.sh | 2 +-
.../uptime/.buildkite/pipelines/flaky.js | 117 ++++++++++++++++++
.../uptime/.buildkite/pipelines/flaky.sh | 8 ++
3 files changed, 126 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/uptime/.buildkite/pipelines/flaky.js
create mode 100755 x-pack/plugins/uptime/.buildkite/pipelines/flaky.sh
diff --git a/.buildkite/scripts/steps/functional/uptime.sh b/.buildkite/scripts/steps/functional/uptime.sh
index 5a59f4dfa48bd..a1c8c2bf6c85b 100755
--- a/.buildkite/scripts/steps/functional/uptime.sh
+++ b/.buildkite/scripts/steps/functional/uptime.sh
@@ -14,4 +14,4 @@ echo "--- Uptime @elastic/synthetics Tests"
cd "$XPACK_DIR"
checks-reporter-with-killswitch "Uptime @elastic/synthetics Tests" \
- node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION"
+ node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" ${GREP:+--grep \"${GREP}\"}
diff --git a/x-pack/plugins/uptime/.buildkite/pipelines/flaky.js b/x-pack/plugins/uptime/.buildkite/pipelines/flaky.js
new file mode 100644
index 0000000000000..6e12f8ca3c921
--- /dev/null
+++ b/x-pack/plugins/uptime/.buildkite/pipelines/flaky.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+const { execSync } = require('child_process');
+
+// List of steps generated dynamically from this jobs
+const steps = [];
+const pipeline = {
+ env: {
+ IGNORE_SHIP_CI_STATS_ERROR: 'true',
+ },
+ steps: steps,
+};
+
+// Default config
+const defaultCount = 25;
+const maxCount = 500;
+const defaultConcurrency = 25;
+const maxConcurrency = 50;
+const initialJobs = 2;
+
+const UUID = process.env.UUID;
+const KIBANA_BUILD_ID = 'KIBANA_BUILD_ID';
+const BUILD_UUID = 'build';
+
+// Metada keys, should match the ones specified in pipeline step configuration
+const E2E_COUNT = 'e2e/count';
+const E2E_CONCURRENCY = 'e2e/concurrent';
+const E2E_GREP = 'e2e/grep';
+const E2E_ARTIFACTS_ID = 'e2e/build-id';
+
+const env = getEnvFromMetadata();
+
+const totalJobs = env[E2E_COUNT] + initialJobs;
+
+if (totalJobs > maxCount) {
+ console.error('+++ Too many steps');
+ console.error(
+ `Buildkite builds can only contain 500 steps in total. Found ${totalJobs} in total. Make sure your test runs are less than ${
+ maxCount - initialJobs
+ }`
+ );
+ process.exit(1);
+}
+
+// If build id is provided, export it so build step is skipped
+pipeline.env[KIBANA_BUILD_ID] = env[E2E_ARTIFACTS_ID];
+
+// Build job first
+steps.push(getBuildJob());
+steps.push(getGroupRunnerJob(env));
+
+console.log(JSON.stringify(pipeline, null, 2));
+
+/***
+ * Utils
+ */
+
+function getBuildJob() {
+ return {
+ command: '.buildkite/scripts/steps/build_kibana.sh',
+ label: 'Build Kibana Distribution and Plugins',
+ agents: { queue: 'c2-8' },
+ key: BUILD_UUID,
+ if: `build.env('${KIBANA_BUILD_ID}') == null || build.env('${KIBANA_BUILD_ID}') == ''`,
+ };
+}
+
+function getGroupRunnerJob(env) {
+ return {
+ command: `${
+ env[E2E_GREP] ? `GREP="${env[E2E_GREP]}" ` : ''
+ }.buildkite/scripts/steps/functional/uptime.sh`,
+ label: `Uptime E2E - Synthetics runner`,
+ agents: { queue: 'n2-4' },
+ depends_on: BUILD_UUID,
+ parallelism: env[E2E_COUNT],
+ concurrency: env[E2E_CONCURRENCY],
+ concurrency_group: UUID,
+ concurrency_method: 'eager',
+ };
+}
+
+function getEnvFromMetadata() {
+ const env = {};
+
+ env[E2E_COUNT] = getIntValue(E2E_COUNT, defaultCount);
+ env[E2E_CONCURRENCY] = getIntValue(E2E_CONCURRENCY, defaultConcurrency);
+ env[E2E_GREP] = getStringValue(E2E_GREP);
+ env[E2E_ARTIFACTS_ID] = getStringValue(E2E_ARTIFACTS_ID);
+
+ env[E2E_CONCURRENCY] =
+ env[E2E_CONCURRENCY] > maxConcurrency ? maxConcurrency : env[E2E_CONCURRENCY];
+
+ return env;
+}
+
+function getIntValue(key, defaultValue) {
+ let value = defaultValue;
+ const cli = execSync(`buildkite-agent meta-data get '${key}' --default ${defaultValue} `)
+ .toString()
+ .trim();
+
+ try {
+ value = parseInt(cli, 10);
+ } finally {
+ return value;
+ }
+}
+
+function getStringValue(key) {
+ return execSync(`buildkite-agent meta-data get '${key}' --default ''`).toString().trim();
+}
diff --git a/x-pack/plugins/uptime/.buildkite/pipelines/flaky.sh b/x-pack/plugins/uptime/.buildkite/pipelines/flaky.sh
new file mode 100755
index 0000000000000..742435f6bec28
--- /dev/null
+++ b/x-pack/plugins/uptime/.buildkite/pipelines/flaky.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+UUID="$(cat /proc/sys/kernel/random/uuid)"
+export UUID
+
+node x-pack/plugins/uptime/.buildkite/pipelines/flaky.js | buildkite-agent pipeline upload
From e397dabe62914cff2feb0d7722a6a645b1d36cab Mon Sep 17 00:00:00 2001
From: Paulo Henrique
Date: Fri, 4 Mar 2022 13:54:37 -0300
Subject: [PATCH 04/27] [Security Solution] Session View Plugin (#124575)
Co-authored-by: mitodrummer
Co-authored-by: Jan Monschke
Co-authored-by: Paulo Henrique
Co-authored-by: Jack
Co-authored-by: Karl Godard
Co-authored-by: Jiawei Wu
Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
Co-authored-by: Ricky Ang
Co-authored-by: Rickyanto Ang
Co-authored-by: Jack
Co-authored-by: Maxwell Borden
Co-authored-by: Maxwell Borden
---
.github/CODEOWNERS | 3 +
docs/developer/plugin-list.asciidoc | 4 +
package.json | 2 +-
packages/kbn-optimizer/limits.yml | 3 +-
x-pack/.i18nrc.json | 1 +
x-pack/plugins/session_view/.eslintrc.json | 5 +
x-pack/plugins/session_view/README.md | 36 +
.../plugins/session_view/common/constants.ts | 27 +
.../constants/session_view_process.mock.ts | 951 +++++++++++++
.../session_view_process_events.mock.ts | 1236 +++++++++++++++++
.../common/types/process_tree/index.ts | 163 +++
.../common/utils/expand_dotted_object.test.ts | 41 +
.../common/utils/expand_dotted_object.ts | 52 +
.../common/utils/sort_processes.test.ts | 30 +
.../common/utils/sort_processes.ts | 23 +
x-pack/plugins/session_view/jest.config.js | 18 +
x-pack/plugins/session_view/kibana.json | 19 +
x-pack/plugins/session_view/package.json | 11 +
.../detail_panel_accordion/index.test.tsx | 77 +
.../detail_panel_accordion/index.tsx | 76 +
.../detail_panel_accordion/styles.ts | 40 +
.../detail_panel_copy/index.test.tsx | 33 +
.../components/detail_panel_copy/index.tsx | 59 +
.../components/detail_panel_copy/styles.ts | 30 +
.../index.test.tsx | 51 +
.../detail_panel_description_list/index.tsx | 33 +
.../detail_panel_description_list/styles.ts | 40 +
.../detail_panel_host_tab/index.test.tsx | 88 ++
.../detail_panel_host_tab/index.tsx | 161 +++
.../detail_panel_list_item/index.test.tsx | 61 +
.../detail_panel_list_item/index.tsx | 51 +
.../detail_panel_list_item/styles.ts | 46 +
.../detail_panel_process_tab/helpers.test.ts | 36 +
.../detail_panel_process_tab/helpers.ts | 28 +
.../detail_panel_process_tab/index.test.tsx | 79 ++
.../detail_panel_process_tab/index.tsx | 255 ++++
.../detail_panel_process_tab/styles.ts | 41 +
.../components/process_tree/helpers.test.ts | 76 +
.../public/components/process_tree/helpers.ts | 170 +++
.../components/process_tree/hooks.test.tsx | 29 +
.../public/components/process_tree/hooks.ts | 255 ++++
.../components/process_tree/index.test.tsx | 91 ++
.../public/components/process_tree/index.tsx | 179 +++
.../public/components/process_tree/styles.ts | 49 +
.../process_tree_alerts/index.test.tsx | 54 +
.../components/process_tree_alerts/index.tsx | 95 ++
.../components/process_tree_alerts/styles.ts | 45 +
.../components/process_tree_node/buttons.tsx | 105 ++
.../process_tree_node/index.test.tsx | 200 +++
.../components/process_tree_node/index.tsx | 213 +++
.../components/process_tree_node/styles.ts | 118 ++
.../process_tree_node/use_button_styles.ts | 62 +
.../public/components/session_view/hooks.ts | 91 ++
.../components/session_view/index.test.tsx | 104 ++
.../public/components/session_view/index.tsx | 205 +++
.../public/components/session_view/styles.ts | 36 +
.../session_view_detail_panel/helpers.ts | 63 +
.../session_view_detail_panel/index.test.tsx | 40 +
.../session_view_detail_panel/index.tsx | 82 ++
.../session_view_search_bar/index.test.tsx | 95 ++
.../session_view_search_bar/index.tsx | 70 +
.../session_view_search_bar/styles.ts | 28 +
.../session_view/public/hooks/use_scroll.ts | 51 +
x-pack/plugins/session_view/public/index.ts | 12 +
.../session_view/public/methods/index.tsx | 25 +
x-pack/plugins/session_view/public/plugin.ts | 22 +
.../session_view/public/shared_imports.ts | 8 +
.../session_view/public/test/index.tsx | 137 ++
x-pack/plugins/session_view/public/types.ts | 49 +
.../public/utils/data_or_dash.test.ts | 30 +
.../session_view/public/utils/data_or_dash.ts | 22 +
x-pack/plugins/session_view/server/index.ts | 13 +
x-pack/plugins/session_view/server/plugin.ts | 44 +
.../session_view/server/routes/index.ts | 14 +
.../routes/process_events_route.test.ts | 57 +
.../server/routes/process_events_route.ts | 85 ++
.../routes/session_entry_leaders_route.ts | 37 +
x-pack/plugins/session_view/server/types.ts | 11 +
x-pack/plugins/session_view/tsconfig.json | 42 +
yarn.lock | 8 +-
80 files changed, 7126 insertions(+), 6 deletions(-)
create mode 100644 x-pack/plugins/session_view/.eslintrc.json
create mode 100644 x-pack/plugins/session_view/README.md
create mode 100644 x-pack/plugins/session_view/common/constants.ts
create mode 100644 x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts
create mode 100644 x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts
create mode 100644 x-pack/plugins/session_view/common/types/process_tree/index.ts
create mode 100644 x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts
create mode 100644 x-pack/plugins/session_view/common/utils/expand_dotted_object.ts
create mode 100644 x-pack/plugins/session_view/common/utils/sort_processes.test.ts
create mode 100644 x-pack/plugins/session_view/common/utils/sort_processes.ts
create mode 100644 x-pack/plugins/session_view/jest.config.js
create mode 100644 x-pack/plugins/session_view/kibana.json
create mode 100644 x-pack/plugins/session_view/package.json
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.ts
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/helpers.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/hooks.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/session_view/hooks.ts
create mode 100644 x-pack/plugins/session_view/public/components/session_view/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view/styles.ts
create mode 100644 x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts
create mode 100644 x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx
create mode 100644 x-pack/plugins/session_view/public/components/session_view_search_bar/styles.ts
create mode 100644 x-pack/plugins/session_view/public/hooks/use_scroll.ts
create mode 100644 x-pack/plugins/session_view/public/index.ts
create mode 100644 x-pack/plugins/session_view/public/methods/index.tsx
create mode 100644 x-pack/plugins/session_view/public/plugin.ts
create mode 100644 x-pack/plugins/session_view/public/shared_imports.ts
create mode 100644 x-pack/plugins/session_view/public/test/index.tsx
create mode 100644 x-pack/plugins/session_view/public/types.ts
create mode 100644 x-pack/plugins/session_view/public/utils/data_or_dash.test.ts
create mode 100644 x-pack/plugins/session_view/public/utils/data_or_dash.ts
create mode 100644 x-pack/plugins/session_view/server/index.ts
create mode 100644 x-pack/plugins/session_view/server/plugin.ts
create mode 100644 x-pack/plugins/session_view/server/routes/index.ts
create mode 100644 x-pack/plugins/session_view/server/routes/process_events_route.test.ts
create mode 100644 x-pack/plugins/session_view/server/routes/process_events_route.ts
create mode 100644 x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts
create mode 100644 x-pack/plugins/session_view/server/types.ts
create mode 100644 x-pack/plugins/session_view/tsconfig.json
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 63e335067199d..691daa042bba9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -416,6 +416,9 @@ x-pack/plugins/security_solution/cypress/upgrade_integration @elastic/security-e
x-pack/plugins/security_solution/cypress/README.md @elastic/security-engineering-productivity
x-pack/test/security_solution_cypress @elastic/security-engineering-productivity
+## Security Solution sub teams - adaptive-workload-protection
+x-pack/plugins/session_view @elastic/awp-platform
+
# Security Intelligence And Analytics
/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 2de3fc3000ac5..c26a748839daf 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -584,6 +584,10 @@ Kibana.
|Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing.
+|{kib-repo}blob/{branch}/x-pack/plugins/session_view/README.md[sessionView]
+|Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time.
+
+
|{kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore]
|or
diff --git a/package.json b/package.json
index 6c313ac834af7..baf1103a8ef5c 100644
--- a/package.json
+++ b/package.json
@@ -347,7 +347,7 @@
"react-moment-proptypes": "^1.7.0",
"react-monaco-editor": "^0.41.2",
"react-popper-tooltip": "^2.10.1",
- "react-query": "^3.34.0",
+ "react-query": "^3.34.7",
"react-redux": "^7.2.0",
"react-resizable": "^1.7.5",
"react-resize-detector": "^4.2.0",
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index f9f0bfc4fd29e..afe7fcd9ddc86 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -121,5 +121,6 @@ pageLoadAssetSize:
expressionPartitionVis: 26338
sharedUX: 16225
ux: 20784
- visTypeGauge: 24113
+ sessionView: 77750
cloudSecurityPosture: 19109
+ visTypeGauge: 24113
\ No newline at end of file
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index c48041c1e1883..dfe34988c4d27 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -52,6 +52,7 @@
"xpack.security": "plugins/security",
"xpack.server": "legacy/server",
"xpack.securitySolution": "plugins/security_solution",
+ "xpack.sessionView": "plugins/session_view",
"xpack.snapshotRestore": "plugins/snapshot_restore",
"xpack.spaces": "plugins/spaces",
"xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"],
diff --git a/x-pack/plugins/session_view/.eslintrc.json b/x-pack/plugins/session_view/.eslintrc.json
new file mode 100644
index 0000000000000..2aab6c2d9093b
--- /dev/null
+++ b/x-pack/plugins/session_view/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "@typescript-eslint/consistent-type-definitions": 0
+ }
+}
diff --git a/x-pack/plugins/session_view/README.md b/x-pack/plugins/session_view/README.md
new file mode 100644
index 0000000000000..384be8bcc292b
--- /dev/null
+++ b/x-pack/plugins/session_view/README.md
@@ -0,0 +1,36 @@
+# Session View
+
+Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time.
+
+It provides an audit trail of:
+
+- Interactive processes being entered by a user into the terminal - User Input
+- Processes and services which do not have a controlling tty (ie are not interactive)
+- Output which is generated as a result of process activity - Output
+- Nested sessions inside the entry session - Nested session (Note: For now nested sessions will display as they did at Cmd with no special handling for TMUX)
+- Full telemetry about the process initiated event. This will include the information specified in the Linux logical event model
+- Who executed the session or process, even if the user changes.
+
+## Development
+
+## Tests
+
+### Unit tests
+
+From kibana path in your terminal go to this plugin root:
+
+```bash
+cd x-pack/plugins/session_view
+```
+
+Then run jest with:
+
+```bash
+yarn test:jest
+```
+
+Or if running from kibana root, you can specify the `-i` to specify the path:
+
+```bash
+yarn test:jest -i x-pack/plugins/session_view/
+```
diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts
new file mode 100644
index 0000000000000..5baf690dc44a5
--- /dev/null
+++ b/x-pack/plugins/session_view/common/constants.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.
+ */
+
+export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route';
+export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route';
+export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default';
+export const ALERTS_INDEX = '.siem-signals-default';
+export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id';
+
+// We fetch a large number of events per page to mitigate a few design caveats in session viewer
+// 1. Due to the hierarchical nature of the data (e.g we are rendering a time ordered pid tree) there are common scenarios where there
+// are few top level processes, but many nested children. For example, a build script is run on a remote host via ssh. If for example our page
+// size is 10 and the build script has 500 nested children, the user would see a load more button that they could continously click without seeing
+// anychange since the next 10 events would be for processes nested under a top level process that might not be expanded. That being said, it's quite
+// possible there are build scripts with many thousands of events, in which case this initial large page will have the same issue. A technique used
+// in previous incarnations of session view included auto expanding the node which is receiving the new page of events so as to not confuse the user.
+// We may need to include this trick as part of this implementation as well.
+// 2. The plain text search that comes with Session view is currently limited in that it only searches through data that has been loaded into the browser.
+// The large page size allows the user to get a broader set of results per page. That being said, this feature is kind of flawed since sessions could be many thousands
+// if not 100s of thousands of events, and to be required to page through these sessions to find more search matches is not a great experience. Future iterations of the
+// search functionality will instead use a separate ES backend search to avoid this.
+// 3. Fewer round trips to the backend!
+export const PROCESS_EVENTS_PER_PAGE = 1000;
diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts
new file mode 100644
index 0000000000000..b7b0bbb91b5ec
--- /dev/null
+++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts
@@ -0,0 +1,951 @@
+/*
+ * 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 {
+ Process,
+ ProcessEvent,
+ ProcessEventsPage,
+ ProcessFields,
+ EventAction,
+ EventKind,
+ ProcessMap,
+} from '../../types/process_tree';
+
+export const mockEvents: ProcessEvent[] = [
+ {
+ '@timestamp': '2021-11-23T15:25:04.210Z',
+ user: {
+ name: '',
+ id: '1000',
+ },
+ process: {
+ pid: 3535,
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: false,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ parent: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ name: '',
+ args_count: 0,
+ args: [],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ },
+ event: {
+ action: EventAction.fork,
+ category: 'process',
+ kind: EventKind.event,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ },
+ {
+ '@timestamp': '2021-11-23T15:25:04.218Z',
+ user: {
+ name: '',
+ id: '1000',
+ },
+ process: {
+ pid: 3535,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ parent: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.218Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.218Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.218Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ pid: 3535,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.218Z',
+ },
+ event: {
+ action: EventAction.exec,
+ category: 'process',
+ kind: EventKind.event,
+ },
+ },
+ {
+ '@timestamp': '2021-11-23T15:25:05.202Z',
+ user: {
+ name: '',
+ id: '1000',
+ },
+ process: {
+ pid: 3535,
+ exit_code: 137,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3728',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ parent: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:05.202Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:05.202Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:05.202Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ pid: 3535,
+ exit_code: 137,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3728',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ start: '2021-11-23T15:25:05.202Z',
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ },
+ event: {
+ action: EventAction.end,
+ category: 'process',
+ kind: EventKind.event,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ },
+] as ProcessEvent[];
+
+export const mockAlerts: ProcessEvent[] = [
+ {
+ kibana: {
+ alert: {
+ rule: {
+ category: 'Custom Query Rule',
+ consumer: 'siem',
+ name: 'cmd test alert',
+ uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf',
+ enabled: true,
+ description: 'cmd test alert',
+ risk_score: 21,
+ severity: 'low',
+ query: "process.executable: '/usr/bin/vi'",
+ },
+ status: 'active',
+ workflow_status: 'open',
+ reason: 'process event created low alert cmd test alert.',
+ original_time: new Date('2021-11-23T15:25:04.218Z'),
+ original_event: {
+ action: 'exec',
+ },
+ uuid: '6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38',
+ },
+ },
+ '@timestamp': '2021-11-23T15:26:34.859Z',
+ user: {
+ name: 'vagrant',
+ id: '1000',
+ },
+ process: {
+ pid: 3535,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
+ parent: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.859Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.859Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.859Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args: [],
+ args_count: 0,
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.859Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.859Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ event: {
+ action: EventAction.exec,
+ category: 'process',
+ kind: EventKind.signal,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ },
+ {
+ kibana: {
+ alert: {
+ rule: {
+ category: 'Custom Query Rule',
+ consumer: 'siem',
+ name: 'cmd test alert',
+ uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf',
+ enabled: true,
+ description: 'cmd test alert',
+ risk_score: 21,
+ severity: 'low',
+ query: "process.executable: '/usr/bin/vi'",
+ },
+ status: 'active',
+ workflow_status: 'open',
+ reason: 'process event created low alert cmd test alert.',
+ original_time: new Date('2021-11-23T15:25:05.202Z'),
+ original_event: {
+ action: 'exit',
+ },
+ uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
+ },
+ },
+ '@timestamp': '2021-11-23T15:26:34.860Z',
+ user: {
+ name: 'vagrant',
+ id: '1000',
+ },
+ process: {
+ pid: 3535,
+ exit_code: 137,
+ executable: '/usr/bin/vi',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
+ parent: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.860Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.860Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.860Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ pid: 2442,
+ user: {
+ name: '',
+ id: '1000',
+ },
+ executable: '/usr/bin/bash',
+ command_line: 'bash',
+ interactive: true,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.860Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ name: '',
+ args_count: 2,
+ args: ['vi', 'cmd/config.ini'],
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:26:34.860Z',
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ event: {
+ action: EventAction.end,
+ category: 'process',
+ kind: EventKind.signal,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ },
+];
+
+export const mockData: ProcessEventsPage[] = [
+ {
+ events: mockEvents,
+ cursor: '2021-11-23T15:25:04.210Z',
+ },
+];
+
+export const childProcessMock: Process = {
+ id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bd',
+ events: [],
+ children: [],
+ autoExpand: false,
+ searchMatched: null,
+ parent: undefined,
+ orphans: [],
+ addEvent: (_) => undefined,
+ clearSearch: () => undefined,
+ getChildren: () => [],
+ hasOutput: () => false,
+ hasAlerts: () => false,
+ getAlerts: () => [],
+ hasExec: () => false,
+ getOutput: () => '',
+ getDetails: () =>
+ ({
+ '@timestamp': '2021-11-23T15:25:05.210Z',
+ event: {
+ kind: EventKind.event,
+ category: 'process',
+ action: EventAction.exec,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ user: {
+ id: '1',
+ name: 'vagrant',
+ },
+ process: {
+ args: ['ls', '-l'],
+ args_count: 2,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bd',
+ executable: '/bin/ls',
+ interactive: true,
+ name: 'ls',
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:05.210Z',
+ pid: 2,
+ parent: {
+ args: ['bash'],
+ args_count: 1,
+ entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
+ executable: '/bin/bash',
+ interactive: false,
+ name: '',
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ pid: 1,
+ user: {
+ id: '1',
+ name: 'vagrant',
+ },
+ },
+ session_leader: {} as ProcessFields,
+ entry_leader: {} as ProcessFields,
+ group_leader: {} as ProcessFields,
+ },
+ } as ProcessEvent),
+ isUserEntered: () => false,
+ getMaxAlertLevel: () => null,
+};
+
+export const processMock: Process = {
+ id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
+ events: [],
+ children: [],
+ autoExpand: false,
+ searchMatched: null,
+ parent: undefined,
+ orphans: [],
+ addEvent: (_) => undefined,
+ clearSearch: () => undefined,
+ getChildren: () => [],
+ hasOutput: () => false,
+ hasAlerts: () => false,
+ getAlerts: () => [],
+ hasExec: () => false,
+ getOutput: () => '',
+ getDetails: () =>
+ ({
+ '@timestamp': '2021-11-23T15:25:04.210Z',
+ event: {
+ kind: EventKind.event,
+ category: 'process',
+ action: EventAction.exec,
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ user: {
+ id: '1',
+ name: 'vagrant',
+ },
+ process: {
+ args: ['bash'],
+ args_count: 1,
+ entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
+ executable: '/bin/bash',
+ exit_code: 137,
+ interactive: false,
+ name: '',
+ working_directory: '/home/vagrant',
+ start: '2021-11-23T15:25:04.210Z',
+ pid: 1,
+ parent: {} as ProcessFields,
+ session_leader: {} as ProcessFields,
+ entry_leader: {} as ProcessFields,
+ group_leader: {} as ProcessFields,
+ },
+ } as ProcessEvent),
+ isUserEntered: () => false,
+ getMaxAlertLevel: () => null,
+};
+
+export const sessionViewBasicProcessMock: Process = {
+ ...processMock,
+ events: mockEvents,
+ hasExec: () => true,
+ isUserEntered: () => true,
+};
+
+export const sessionViewAlertProcessMock: Process = {
+ ...processMock,
+ events: [...mockEvents, ...mockAlerts],
+ hasAlerts: () => true,
+ getAlerts: () => mockEvents,
+ hasExec: () => true,
+ isUserEntered: () => true,
+};
+
+export const mockProcessMap = mockEvents.reduce(
+ (processMap, event) => {
+ processMap[event.process.entity_id] = {
+ id: event.process.entity_id,
+ events: [event],
+ children: [],
+ parent: undefined,
+ autoExpand: false,
+ searchMatched: null,
+ orphans: [],
+ addEvent: (_) => undefined,
+ clearSearch: () => undefined,
+ getChildren: () => [],
+ hasOutput: () => false,
+ hasAlerts: () => false,
+ getAlerts: () => [],
+ hasExec: () => false,
+ getOutput: () => '',
+ getDetails: () => event,
+ isUserEntered: () => false,
+ getMaxAlertLevel: () => null,
+ };
+ return processMap;
+ },
+ {
+ [sessionViewBasicProcessMock.id]: sessionViewBasicProcessMock,
+ } as ProcessMap
+);
diff --git a/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts b/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts
new file mode 100644
index 0000000000000..47849f859ba9c
--- /dev/null
+++ b/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts
@@ -0,0 +1,1236 @@
+/*
+ * 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 { ProcessEventResults } from '../../types/process_tree';
+
+export const sessionViewProcessEventsMock: ProcessEventResults = {
+ events: [
+ {
+ _index: 'cmd',
+ _id: 'FMUGTX0BGGlsPv9flMF7',
+ _score: null,
+ _source: {
+ '@timestamp': '2021-11-23T13:40:16.528Z',
+ event: {
+ kind: 'event',
+ category: 'process',
+ action: 'fork',
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ Ext: {
+ variant: 'CentOS',
+ },
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ user: {
+ // To keep backwards compat and avoid data duplication. We keep user/group info for top level process at the top level
+ id: '0', // the effective user aka euid
+ name: 'root',
+ real: {
+ // ruid
+ id: '2',
+ name: 'kg',
+ },
+ saved: {
+ // suid
+ id: '2',
+ name: 'kg',
+ },
+ },
+ group: {
+ id: '1', // the effective group aka egid
+ name: 'groupA',
+ real: {
+ // rgid
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ // sgid
+ id: '1',
+ name: 'groupA',
+ },
+ },
+ process: {
+ entity_id: '4321',
+ args: ['/bin/sshd'],
+ args_count: 1,
+ command_line: 'sshd',
+ executable: '/bin/sshd',
+ name: 'sshd',
+ interactive: false,
+ working_directory: '/',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ parent: {
+ entity_id: '4322',
+ args: ['/bin/sshd'],
+ args_count: 1,
+ command_line: 'sshd',
+ executable: '/bin/sshd',
+ name: 'sshd',
+ interactive: true,
+ working_directory: '/',
+ pid: 2,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '2',
+ name: 'kg',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ group_leader: {
+ entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ pid: 1234, // this directly replaces parent.pgid
+ start: '2021-10-14T08:05:34.853Z',
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the session_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the entry_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ entry_meta: {
+ type: 'sshd',
+ source: {
+ ip: '10.132.0.50',
+ geo: {
+ city_name: 'Vancouver',
+ continent_code: 'NA',
+ continent_name: 'North America',
+ country_iso_code: 'CA',
+ country_name: 'Canada',
+ location: {
+ lon: -73.61483,
+ lat: 45.505918,
+ },
+ postal_code: 'V9J1E3',
+ region_iso_code: 'BC',
+ region_name: 'British Columbia',
+ timezone: 'America/Los_Angeles',
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ {
+ descriptor: 1,
+ type: 'pipe',
+ pipe: {
+ inode: '6183207',
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ },
+ sort: [1637674816528],
+ },
+ {
+ _index: 'cmd',
+ _id: 'FsUGTX0BGGlsPv9flMGF',
+ _score: null,
+ _source: {
+ '@timestamp': '2021-11-23T13:40:16.541Z',
+ event: {
+ kind: 'event',
+ category: 'process',
+ action: 'exec',
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ Ext: {
+ variant: 'CentOS',
+ },
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ user: {
+ id: '2',
+ name: 'kg',
+ real: {
+ id: '2',
+ name: 'kg',
+ },
+ saved: {
+ id: '2',
+ name: 'kg',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ process: {
+ entity_id: '4321',
+ args: ['/bin/bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ previous: [{ args: ['/bin/sshd'], args_count: 1, executable: '/bin/sshd' }],
+ parent: {
+ entity_id: '4322',
+ args: ['/bin/sshd'],
+ args_count: 1,
+ command_line: 'sshd',
+ executable: '/bin/sshd',
+ name: 'sshd',
+ interactive: true,
+ working_directory: '/',
+ pid: 2,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ group_leader: {
+ entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ pid: 1234, // this directly replaces parent.pgid
+ start: '2021-10-14T08:05:34.853Z',
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the session_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the entry_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ entry_meta: {
+ type: 'sshd',
+ source: {
+ ip: '10.132.0.50',
+ geo: {
+ city_name: 'Vancouver',
+ continent_code: 'NA',
+ continent_name: 'North America',
+ country_iso_code: 'CA',
+ country_name: 'Canada',
+ location: {
+ lon: -73.61483,
+ lat: 45.505918,
+ },
+ postal_code: 'V9J1E3',
+ region_iso_code: 'BC',
+ region_name: 'British Columbia',
+ timezone: 'America/Los_Angeles',
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ {
+ descriptor: 1,
+ type: 'pipe',
+ pipe: {
+ inode: '6183207',
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ },
+ sort: [1637674816541],
+ },
+ {
+ _index: 'cmd',
+ _id: 'H8UGTX0BGGlsPv9fp8F_',
+ _score: null,
+ _source: {
+ '@timestamp': '2021-11-23T13:40:21.392Z',
+ event: {
+ kind: 'event',
+ category: 'process',
+ action: 'end',
+ },
+ host: {
+ architecture: 'x86_64',
+ hostname: 'james-fleet-714-2',
+ id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
+ ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809',
+ mac: '42:01:0a:84:00:32',
+ name: 'james-fleet-714-2',
+ os: {
+ Ext: {
+ variant: 'CentOS',
+ },
+ family: 'centos',
+ full: 'CentOS 7.9.2009',
+ kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
+ name: 'Linux',
+ platform: 'centos',
+ version: '7.9.2009',
+ },
+ },
+ user: {
+ id: '2',
+ name: 'kg',
+ real: {
+ id: '2',
+ name: 'kg',
+ },
+ saved: {
+ id: '2',
+ name: 'kg',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ process: {
+ entity_id: '4321',
+ args: ['/bin/bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ end: '2021-10-14T10:05:34.853Z',
+ exit_code: 137,
+ previous: [{ args: ['/bin/sshd'], args_count: 1, executable: '/bin/sshd' }],
+ parent: {
+ entity_id: '4322',
+ args: ['/bin/sshd'],
+ args_count: 1,
+ command_line: 'sshd',
+ executable: '/bin/sshd',
+ name: 'sshd',
+ interactive: true,
+ working_directory: '/',
+ pid: 2,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ group_leader: {
+ entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ pid: 1234, // this directly replaces parent.pgid
+ start: '2021-10-14T08:05:34.853Z',
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ group_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ session_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the session_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ entry_leader: {
+ entity_id: '4321',
+ args: ['bash'],
+ args_count: 1,
+ command_line: 'bash',
+ executable: '/bin/bash',
+ name: 'bash',
+ interactive: true,
+ working_directory: '/home/kg',
+ pid: 3,
+ start: '2021-10-14T08:05:34.853Z',
+ user: {
+ id: '0',
+ name: 'root',
+ real: {
+ id: '0',
+ name: 'root',
+ },
+ saved: {
+ id: '0',
+ name: 'root',
+ },
+ },
+ group: {
+ id: '1',
+ name: 'groupA',
+ real: {
+ id: '1',
+ name: 'groupA',
+ },
+ saved: {
+ id: '1',
+ name: 'groupA',
+ },
+ supplemental: [
+ {
+ id: '2',
+ name: 'groupB',
+ },
+ {
+ id: '3',
+ name: 'groupC',
+ },
+ ],
+ },
+ // parent: {
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 2,
+ // start: '2021-10-14T08:05:34.853Z',
+ // session_leader: {
+ // // used as a foreign key to the parent session of the entry_leader
+ // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16',
+ // pid: 4321,
+ // start: '2021-10-14T08:05:34.853Z',
+ // },
+ // },
+ entry_meta: {
+ type: 'sshd',
+ source: {
+ ip: '10.132.0.50',
+ geo: {
+ city_name: 'Vancouver',
+ continent_code: 'NA',
+ continent_name: 'North America',
+ country_iso_code: 'CA',
+ country_name: 'Canada',
+ location: {
+ lon: -73.61483,
+ lat: 45.505918,
+ },
+ postal_code: 'V9J1E3',
+ region_iso_code: 'BC',
+ region_name: 'British Columbia',
+ timezone: 'America/Los_Angeles',
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ file_descriptions: [
+ {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ {
+ descriptor: 1,
+ type: 'pipe',
+ pipe: {
+ inode: '6183207',
+ },
+ },
+ ],
+ tty: {
+ descriptor: 0,
+ type: 'char_device',
+ char_device: {
+ major: 8,
+ minor: 1,
+ },
+ },
+ },
+ },
+ sort: [1637674821392],
+ },
+ ],
+};
diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts
new file mode 100644
index 0000000000000..746c1b2093661
--- /dev/null
+++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 enum EventKind {
+ event = 'event',
+ signal = 'signal',
+}
+
+export const enum EventAction {
+ fork = 'fork',
+ exec = 'exec',
+ end = 'end',
+ output = 'output',
+}
+
+export interface User {
+ id: string;
+ name: string;
+}
+
+export interface ProcessEventResults {
+ events: any[];
+}
+
+export type EntryMetaType =
+ | 'init'
+ | 'sshd'
+ | 'ssm'
+ | 'kubelet'
+ | 'teleport'
+ | 'terminal'
+ | 'console';
+
+export interface EntryMeta {
+ type: EntryMetaType;
+ source: {
+ ip: string;
+ };
+}
+
+export interface Teletype {
+ descriptor: number;
+ type: string;
+ char_device: {
+ major: number;
+ minor: number;
+ };
+}
+
+export interface ProcessFields {
+ entity_id: string;
+ args: string[];
+ args_count: number;
+ command_line: string;
+ executable: string;
+ name: string;
+ interactive: boolean;
+ working_directory: string;
+ pid: number;
+ start: string;
+ end?: string;
+ user: User;
+ exit_code?: number;
+ entry_meta?: EntryMeta;
+ tty: Teletype;
+}
+
+export interface ProcessSelf extends Omit {
+ parent: ProcessFields;
+ session_leader: ProcessFields;
+ entry_leader: ProcessFields;
+ group_leader: ProcessFields;
+}
+
+export interface ProcessEventHost {
+ architecture: string;
+ hostname: string;
+ id: string;
+ ip: string;
+ mac: string;
+ name: string;
+ os: {
+ family: string;
+ full: string;
+ kernel: string;
+ name: string;
+ platform: string;
+ version: string;
+ };
+}
+
+export interface ProcessEventAlertRule {
+ category: string;
+ consumer: string;
+ description: string;
+ enabled: boolean;
+ name: string;
+ query: string;
+ risk_score: number;
+ severity: string;
+ uuid: string;
+}
+
+export interface ProcessEventAlert {
+ uuid: string;
+ reason: string;
+ workflow_status: string;
+ status: string;
+ original_time: Date;
+ original_event: {
+ action: string;
+ };
+ rule: ProcessEventAlertRule;
+}
+
+export interface ProcessEvent {
+ '@timestamp': string;
+ event: {
+ kind: EventKind;
+ category: string;
+ action: EventAction;
+ };
+ user: User;
+ host: ProcessEventHost;
+ process: ProcessSelf;
+ kibana?: {
+ alert: ProcessEventAlert;
+ };
+}
+
+export interface ProcessEventsPage {
+ events: ProcessEvent[];
+ cursor: string;
+}
+
+export interface Process {
+ id: string; // the process entity_id
+ events: ProcessEvent[];
+ children: Process[];
+ orphans: Process[]; // currently, orphans are rendered inline with the entry session leaders children
+ parent: Process | undefined;
+ autoExpand: boolean;
+ searchMatched: string | null; // either false, or set to searchQuery
+ addEvent(event: ProcessEvent): void;
+ clearSearch(): void;
+ hasOutput(): boolean;
+ hasAlerts(): boolean;
+ getAlerts(): ProcessEvent[];
+ hasExec(): boolean;
+ getOutput(): string;
+ getDetails(): ProcessEvent;
+ isUserEntered(): boolean;
+ getMaxAlertLevel(): number | null;
+ getChildren(verboseMode: boolean): Process[];
+}
+
+export type ProcessMap = {
+ [key: string]: Process;
+};
diff --git a/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts b/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts
new file mode 100644
index 0000000000000..a4a4845e759e7
--- /dev/null
+++ b/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.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 { expandDottedObject } from './expand_dotted_object';
+
+const testFlattenedObj = {
+ 'flattened.property.a': 'valueA',
+ 'flattened.property.b': 'valueB',
+ regularProp: {
+ nestedProp: 'nestedValue',
+ },
+ 'nested.array': [
+ {
+ arrayProp: 'arrayValue',
+ },
+ ],
+ emptyArray: [],
+};
+describe('expandDottedObject(obj)', () => {
+ it('retrieves values from flattened keys', () => {
+ const expanded: any = expandDottedObject(testFlattenedObj);
+
+ expect(expanded.flattened.property.a).toEqual('valueA');
+ expect(expanded.flattened.property.b).toEqual('valueB');
+ });
+ it('retrieves values from nested keys', () => {
+ const expanded: any = expandDottedObject(testFlattenedObj);
+
+ expect(Array.isArray(expanded.nested.array)).toBeTruthy();
+ expect(expanded.nested.array[0].arrayProp).toEqual('arrayValue');
+ });
+ it("doesn't break regular value access", () => {
+ const expanded: any = expandDottedObject(testFlattenedObj);
+
+ expect(expanded.regularProp.nestedProp).toEqual('nestedValue');
+ });
+});
diff --git a/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts b/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts
new file mode 100644
index 0000000000000..69a9cb8236cbc
--- /dev/null
+++ b/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { merge } from '@kbn/std';
+
+const expandDottedField = (dottedFieldName: string, val: unknown): object => {
+ const parts = dottedFieldName.split('.');
+ if (parts.length === 1) {
+ return { [parts[0]]: val };
+ } else {
+ return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) };
+ }
+};
+
+/*
+ * Expands an object with "dotted" fields to a nested object with unflattened fields.
+ *
+ * Example:
+ * expandDottedObject({
+ * "kibana.alert.depth": 1,
+ * "kibana.alert.ancestors": [{
+ * id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
+ * type: "event",
+ * index: "signal_index",
+ * depth: 0,
+ * }],
+ * })
+ *
+ * => {
+ * kibana: {
+ * alert: {
+ * ancestors: [
+ * id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
+ * type: "event",
+ * index: "signal_index",
+ * depth: 0,
+ * ],
+ * depth: 1,
+ * },
+ * },
+ * }
+ */
+export const expandDottedObject = (dottedObj: object) => {
+ return Object.entries(dottedObj).reduce(
+ (acc, [key, val]) => merge(acc, expandDottedField(key, val)),
+ {}
+ );
+};
diff --git a/x-pack/plugins/session_view/common/utils/sort_processes.test.ts b/x-pack/plugins/session_view/common/utils/sort_processes.test.ts
new file mode 100644
index 0000000000000..b1db5381954dc
--- /dev/null
+++ b/x-pack/plugins/session_view/common/utils/sort_processes.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 { sortProcesses } from './sort_processes';
+import { mockProcessMap } from '../mocks/constants/session_view_process.mock';
+
+describe('sortProcesses(a, b)', () => {
+ it('sorts processes in ascending order by start time', () => {
+ const processes = Object.values(mockProcessMap);
+
+ // shuffle some things to ensure all sort lines are hit
+ const c = processes[0];
+ processes[0] = processes[processes.length - 1];
+ processes[processes.length - 1] = c;
+
+ processes.sort(sortProcesses);
+
+ for (let i = 0; i < processes.length - 1; i++) {
+ const current = processes[i];
+ const next = processes[i + 1];
+ expect(
+ new Date(next.getDetails().process.start) >= new Date(current.getDetails().process.start)
+ ).toBeTruthy();
+ }
+ });
+});
diff --git a/x-pack/plugins/session_view/common/utils/sort_processes.ts b/x-pack/plugins/session_view/common/utils/sort_processes.ts
new file mode 100644
index 0000000000000..a0a42590e457e
--- /dev/null
+++ b/x-pack/plugins/session_view/common/utils/sort_processes.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 { Process } from '../types/process_tree';
+
+export const sortProcesses = (a: Process, b: Process) => {
+ const eventAStartTime = new Date(a.getDetails().process.start);
+ const eventBStartTime = new Date(b.getDetails().process.start);
+
+ if (eventAStartTime < eventBStartTime) {
+ return -1;
+ }
+
+ if (eventAStartTime > eventBStartTime) {
+ return 1;
+ }
+
+ return 0;
+};
diff --git a/x-pack/plugins/session_view/jest.config.js b/x-pack/plugins/session_view/jest.config.js
new file mode 100644
index 0000000000000..d35db0d369468
--- /dev/null
+++ b/x-pack/plugins/session_view/jest.config.js
@@ -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.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/x-pack/plugins/session_view'],
+ coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/session_view',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/x-pack/plugins/session_view/{common,public,server}/**/*.{ts,tsx}',
+ ],
+ setupFiles: ['jest-canvas-mock'],
+};
diff --git a/x-pack/plugins/session_view/kibana.json b/x-pack/plugins/session_view/kibana.json
new file mode 100644
index 0000000000000..ff9d849016c55
--- /dev/null
+++ b/x-pack/plugins/session_view/kibana.json
@@ -0,0 +1,19 @@
+{
+ "id": "sessionView",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "owner": {
+ "name": "Security Team",
+ "githubTeam": "security-team"
+ },
+ "requiredPlugins": [
+ "data",
+ "timelines"
+ ],
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared"
+ ],
+ "server": true,
+ "ui": true
+}
diff --git a/x-pack/plugins/session_view/package.json b/x-pack/plugins/session_view/package.json
new file mode 100644
index 0000000000000..2cb3dc882ed71
--- /dev/null
+++ b/x-pack/plugins/session_view/package.json
@@ -0,0 +1,11 @@
+{
+ "author": "Elastic",
+ "name": "session_view",
+ "version": "8.0.0",
+ "private": true,
+ "license": "Elastic-License",
+ "scripts": {
+ "test:jest": "node ../../scripts/jest",
+ "test:coverage": "node ../../scripts/jest --coverage"
+ }
+}
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx
new file mode 100644
index 0000000000000..80ad3ce0c4630
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { DetailPanelAccordion } from './index';
+
+const TEST_ID = 'test';
+const TEST_LIST_ITEM = [
+ {
+ title: 'item title',
+ description: 'item description',
+ },
+];
+const TEST_TITLE = 'accordion title';
+const ACTION_TEXT = 'extra action';
+
+describe('DetailPanelAccordion component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelAccordion is mounted', () => {
+ it('should render basic acoordion', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible();
+ });
+
+ it('should render acoordion with tooltip', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible();
+ expect(
+ renderResult.queryByTestId('sessionView:detail-panel-accordion-tooltip')
+ ).toBeVisible();
+ });
+
+ it('should render acoordion with extra action', async () => {
+ const mockFn = jest.fn();
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible();
+ const extraActionButton = renderResult.getByTestId(
+ 'sessionView:detail-panel-accordion-action'
+ );
+ expect(extraActionButton).toHaveTextContent(ACTION_TEXT);
+ extraActionButton.click();
+ expect(mockFn).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx
new file mode 100644
index 0000000000000..4e03931e4fcd9
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { ReactNode } from 'react';
+import { EuiAccordion, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
+import { useStyles } from './styles';
+import { DetailPanelDescriptionList } from '../detail_panel_description_list';
+
+interface DetailPanelAccordionDeps {
+ id: string;
+ listItems: Array<{
+ title: NonNullable;
+ description: NonNullable;
+ }>;
+ title: string;
+ tooltipContent?: string;
+ extraActionTitle?: string;
+ onExtraActionClick?: () => void;
+}
+
+/**
+ * An accordion section in session view detail panel.
+ */
+export const DetailPanelAccordion = ({
+ id,
+ listItems,
+ title,
+ tooltipContent,
+ extraActionTitle,
+ onExtraActionClick,
+}: DetailPanelAccordionDeps) => {
+ const styles = useStyles();
+
+ return (
+
+
+ {title}
+
+ {tooltipContent && (
+
+
+
+ )}
+
+ }
+ extraAction={
+ extraActionTitle ? (
+
+ {extraActionTitle}
+
+ ) : null
+ }
+ css={styles.accordion}
+ data-test-subj="sessionView:detail-panel-accordion"
+ >
+
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts
new file mode 100644
index 0000000000000..c44e069c05c00
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const tabSection: CSSObject = {
+ padding: euiTheme.size.base,
+ };
+
+ const accordion: CSSObject = {
+ borderTop: euiTheme.border.thin,
+ '&:last-child': {
+ borderBottom: euiTheme.border.thin,
+ },
+ };
+
+ const accordionButton: CSSObject = {
+ padding: euiTheme.size.base,
+ fontWeight: euiTheme.font.weight.bold,
+ };
+
+ return {
+ accordion,
+ accordionButton,
+ tabSection,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx
new file mode 100644
index 0000000000000..bb1dd243621bd
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.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 React from 'react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { DetailPanelCopy } from './index';
+
+const TEST_TEXT_COPY = 'copy component test';
+const TEST_CHILD = {TEST_TEXT_COPY};
+
+describe('DetailPanelCopy component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelCopy is mounted', () => {
+ it('renders DetailPanelCopy correctly', async () => {
+ renderResult = mockedContext.render(
+ {TEST_CHILD}
+ );
+
+ expect(renderResult.queryByText(TEST_TEXT_COPY)).toBeVisible();
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx
new file mode 100644
index 0000000000000..a5ce77894949b
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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, { ReactNode } from 'react';
+import { EuiButtonIcon, EuiCopy } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DetailPanelListItem } from '../detail_panel_list_item';
+import { dataOrDash } from '../../utils/data_or_dash';
+import { useStyles } from './styles';
+
+interface DetailPanelCopyDeps {
+ children: ReactNode;
+ textToCopy: string | number;
+ display?: 'inlineBlock' | 'block' | undefined;
+}
+
+interface DetailPanelListItemProps {
+ copy: ReactNode;
+ display?: string;
+}
+
+/**
+ * Copy to clipboard component in Session view detail panel.
+ */
+export const DetailPanelCopy = ({
+ children,
+ textToCopy,
+ display = 'inlineBlock',
+}: DetailPanelCopyDeps) => {
+ const styles = useStyles();
+
+ const props: DetailPanelListItemProps = {
+ copy: (
+
+ {(copy) => (
+
+ )}
+
+ ),
+ };
+
+ if (display === 'block') {
+ props.display = display;
+ }
+
+ return {children};
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts
new file mode 100644
index 0000000000000..0bfc67dddb885
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const copyButton: CSSObject = {
+ position: 'absolute',
+ right: euiTheme.size.s,
+ top: 0,
+ bottom: 0,
+ margin: 'auto',
+ };
+
+ return {
+ copyButton,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx
new file mode 100644
index 0000000000000..aaf3086aabf5e
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { DetailPanelDescriptionList } from './index';
+
+const TEST_FIRST_TITLE = 'item title';
+const TEST_FIRST_DESCRIPTION = 'item description';
+const TEST_SECOND_TITLE = 'second title';
+const TEST_SECOND_DESCRIPTION = 'second description';
+const TEST_LIST_ITEM = [
+ {
+ title: TEST_FIRST_TITLE,
+ description: TEST_FIRST_DESCRIPTION,
+ },
+ {
+ title: TEST_SECOND_TITLE,
+ description: TEST_SECOND_DESCRIPTION,
+ },
+];
+
+describe('DetailPanelDescriptionList component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelDescriptionList is mounted', () => {
+ it('renders DetailPanelDescriptionList correctly', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.queryByTestId('sessionView:detail-panel-description-list')).toBeVisible();
+
+ // check list items are rendered
+ expect(renderResult.queryByText(TEST_FIRST_TITLE)).toBeVisible();
+ expect(renderResult.queryByText(TEST_FIRST_DESCRIPTION)).toBeVisible();
+ expect(renderResult.queryByText(TEST_SECOND_TITLE)).toBeVisible();
+ expect(renderResult.queryByText(TEST_SECOND_DESCRIPTION)).toBeVisible();
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx
new file mode 100644
index 0000000000000..3d942fc42326e
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.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 React, { ReactNode } from 'react';
+import { EuiDescriptionList } from '@elastic/eui';
+import { useStyles } from './styles';
+
+interface DetailPanelDescriptionListDeps {
+ listItems: Array<{
+ title: NonNullable;
+ description: NonNullable;
+ }>;
+}
+
+/**
+ * Description list in session view detail panel.
+ */
+export const DetailPanelDescriptionList = ({ listItems }: DetailPanelDescriptionListDeps) => {
+ const styles = useStyles();
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts
new file mode 100644
index 0000000000000..d815cb2a48283
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.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 { useMemo } from 'react';
+import { CSSObject } from '@emotion/react';
+import { useEuiTheme } from '@elastic/eui';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const descriptionList: CSSObject = {
+ padding: euiTheme.size.s,
+ };
+
+ const tabListTitle = {
+ width: '40%',
+ display: 'flex',
+ alignItems: 'center',
+ };
+
+ const tabListDescription = {
+ width: '60%',
+ display: 'flex',
+ alignItems: 'center',
+ };
+
+ return {
+ descriptionList,
+ tabListTitle,
+ tabListDescription,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx
new file mode 100644
index 0000000000000..2df9f47e5a416
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { ProcessEventHost } from '../../../common/types/process_tree';
+import { DetailPanelHostTab } from './index';
+
+const TEST_ARCHITECTURE = 'x86_64';
+const TEST_HOSTNAME = 'host-james-fleet-714-2';
+const TEST_ID = '48c1b3f1ac5da4e0057fc9f60f4d1d5d';
+const TEST_IP = '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809';
+const TEST_MAC = '42:01:0a:84:00:32';
+const TEST_NAME = 'name-james-fleet-714-2';
+const TEST_OS_FAMILY = 'family-centos';
+const TEST_OS_FULL = 'full-CentOS 7.9.2009';
+const TEST_OS_KERNEL = '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021';
+const TEST_OS_NAME = 'os-Linux';
+const TEST_OS_PLATFORM = 'platform-centos';
+const TEST_OS_VERSION = 'version-7.9.2009';
+
+const TEST_HOST: ProcessEventHost = {
+ architecture: TEST_ARCHITECTURE,
+ hostname: TEST_HOSTNAME,
+ id: TEST_ID,
+ ip: TEST_IP,
+ mac: TEST_MAC,
+ name: TEST_NAME,
+ os: {
+ family: TEST_OS_FAMILY,
+ full: TEST_OS_FULL,
+ kernel: TEST_OS_KERNEL,
+ name: TEST_OS_NAME,
+ platform: TEST_OS_PLATFORM,
+ version: TEST_OS_VERSION,
+ },
+};
+
+describe('DetailPanelHostTab component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelHostTab is mounted', () => {
+ it('renders DetailPanelHostTab correctly', async () => {
+ renderResult = mockedContext.render();
+
+ expect(renderResult.queryByText('architecture')).toBeVisible();
+ expect(renderResult.queryByText('hostname')).toBeVisible();
+ expect(renderResult.queryByText('id')).toBeVisible();
+ expect(renderResult.queryByText('ip')).toBeVisible();
+ expect(renderResult.queryByText('mac')).toBeVisible();
+ expect(renderResult.queryByText('name')).toBeVisible();
+ expect(renderResult.queryByText(TEST_ARCHITECTURE)).toBeVisible();
+ expect(renderResult.queryByText(TEST_HOSTNAME)).toBeVisible();
+ expect(renderResult.queryByText(TEST_ID)).toBeVisible();
+ expect(renderResult.queryByText(TEST_IP)).toBeVisible();
+ expect(renderResult.queryByText(TEST_MAC)).toBeVisible();
+ expect(renderResult.queryByText(TEST_NAME)).toBeVisible();
+
+ // expand host os accordion
+ renderResult
+ .queryByTestId('sessionView:detail-panel-accordion')
+ ?.querySelector('button')
+ ?.click();
+ expect(renderResult.queryByText('os.family')).toBeVisible();
+ expect(renderResult.queryByText('os.full')).toBeVisible();
+ expect(renderResult.queryByText('os.kernel')).toBeVisible();
+ expect(renderResult.queryByText('os.name')).toBeVisible();
+ expect(renderResult.queryByText('os.platform')).toBeVisible();
+ expect(renderResult.queryByText('os.version')).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_FAMILY)).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_FULL)).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_KERNEL)).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_NAME)).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_PLATFORM)).toBeVisible();
+ expect(renderResult.queryByText(TEST_OS_VERSION)).toBeVisible();
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx
new file mode 100644
index 0000000000000..e46e0e2751872
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx
@@ -0,0 +1,161 @@
+/*
+ * 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 { EuiTextColor } from '@elastic/eui';
+import { ProcessEventHost } from '../../../common/types/process_tree';
+import { DetailPanelAccordion } from '../detail_panel_accordion';
+import { DetailPanelCopy } from '../detail_panel_copy';
+import { DetailPanelDescriptionList } from '../detail_panel_description_list';
+import { DetailPanelListItem } from '../detail_panel_list_item';
+import { dataOrDash } from '../../utils/data_or_dash';
+import { useStyles } from '../detail_panel_process_tab/styles';
+
+interface DetailPanelHostTabDeps {
+ processHost: ProcessEventHost;
+}
+
+/**
+ * Host Panel of session view detail panel.
+ */
+export const DetailPanelHostTab = ({ processHost }: DetailPanelHostTabDeps) => {
+ const styles = useStyles();
+
+ return (
+ <>
+ hostname,
+ description: (
+
+
+ {dataOrDash(processHost.hostname)}
+
+
+ ),
+ },
+ {
+ title: id,
+ description: (
+
+
+ {dataOrDash(processHost.id)}
+
+
+ ),
+ },
+ {
+ title: ip,
+ description: (
+
+
+ {dataOrDash(processHost.ip)}
+
+
+ ),
+ },
+ {
+ title: mac,
+ description: (
+
+
+ {dataOrDash(processHost.mac)}
+
+
+ ),
+ },
+ {
+ title: name,
+ description: (
+
+
+ {dataOrDash(processHost.name)}
+
+
+ ),
+ },
+ ]}
+ />
+ architecture,
+ description: (
+
+
+ {dataOrDash(processHost.architecture)}
+
+
+ ),
+ },
+ {
+ title: os.family,
+ description: (
+
+
+ {dataOrDash(processHost.os.family)}
+
+
+ ),
+ },
+ {
+ title: os.full,
+ description: (
+
+
+ {dataOrDash(processHost.os.full)}
+
+
+ ),
+ },
+ {
+ title: os.kernel,
+ description: (
+
+
+ {dataOrDash(processHost.os.kernel)}
+
+
+ ),
+ },
+ {
+ title: os.name,
+ description: (
+
+
+ {dataOrDash(processHost.os.name)}
+
+
+ ),
+ },
+ {
+ title: os.platform,
+ description: (
+
+
+ {dataOrDash(processHost.os.platform)}
+
+
+ ),
+ },
+ {
+ title: os.version,
+ description: (
+
+
+ {dataOrDash(processHost.os.version)}
+
+
+ ),
+ },
+ ]}
+ />
+ >
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx
new file mode 100644
index 0000000000000..e6572a097d85a
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { DetailPanelListItem } from './index';
+
+const TEST_STRING = 'item title';
+const TEST_CHILD = {TEST_STRING};
+const TEST_COPY_STRING = 'test copy button';
+const BUTTON_TEST_ID = 'sessionView:test-copy-button';
+const TEST_COPY = ;
+const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item';
+const WAIT_TIMEOUT = 500;
+
+describe('DetailPanelListItem component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelListItem is mounted', () => {
+ it('renders DetailPanelListItem correctly', async () => {
+ renderResult = mockedContext.render({TEST_CHILD});
+
+ expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible();
+ expect(renderResult.queryByText(TEST_STRING)).toBeVisible();
+ });
+
+ it('renders copy element correctly', async () => {
+ renderResult = mockedContext.render(
+ {TEST_CHILD}
+ );
+
+ expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull();
+ fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID));
+ await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID));
+ expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible();
+
+ fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID));
+ expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull();
+ });
+
+ it('does not have mouse events when copy prop is not present', async () => {
+ renderResult = mockedContext.render({TEST_CHILD});
+
+ expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull();
+ fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID));
+ await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT });
+ expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull();
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx
new file mode 100644
index 0000000000000..93a6554bbe54a
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useState, ReactNode } from 'react';
+import { EuiText, EuiTextProps } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+import { useStyles } from './styles';
+
+interface DetailPanelListItemDeps {
+ children: ReactNode;
+ copy?: ReactNode;
+ display?: string;
+}
+
+interface EuiTextPropsCss extends EuiTextProps {
+ css: CSSObject;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
+}
+
+/**
+ * Detail panel description list item.
+ */
+export const DetailPanelListItem = ({
+ children,
+ copy,
+ display = 'flex',
+}: DetailPanelListItemDeps) => {
+ const [isHovered, setIsHovered] = useState(false);
+ const styles = useStyles({ display });
+
+ const props: EuiTextPropsCss = {
+ size: 's',
+ css: !!copy ? styles.copiableItem : styles.item,
+ };
+
+ if (!!copy) {
+ props.onMouseEnter = () => setIsHovered(true);
+ props.onMouseLeave = () => setIsHovered(false);
+ }
+
+ return (
+
+ {children}
+ {isHovered && copy}
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts
new file mode 100644
index 0000000000000..c370bd8adb6e2
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.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 { useMemo } from 'react';
+import { useEuiTheme, transparentize } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+interface StylesDeps {
+ display: string | undefined;
+}
+
+export const useStyles = ({ display }: StylesDeps) => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const item: CSSObject = {
+ display,
+ alignItems: 'center',
+ padding: euiTheme.size.s,
+ width: '100%',
+ fontSize: 'inherit',
+ fontWeight: 'inherit',
+ minHeight: '36px',
+ };
+
+ const copiableItem: CSSObject = {
+ ...item,
+ position: 'relative',
+ borderRadius: euiTheme.border.radius.medium,
+ '&:hover': {
+ background: transparentize(euiTheme.colors.primary, 0.1),
+ },
+ };
+
+ return {
+ item,
+ copiableItem,
+ };
+ }, [display, euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts
new file mode 100644
index 0000000000000..d458ee3a1d666
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { getProcessExecutableCopyText } from './helpers';
+
+describe('detail panel process tab helpers tests', () => {
+ it('getProcessExecutableCopyText works with empty array', () => {
+ const result = getProcessExecutableCopyText([]);
+ expect(result).toEqual('');
+ });
+
+ it('getProcessExecutableCopyText works with array of tuples', () => {
+ const result = getProcessExecutableCopyText([
+ ['echo', 'exec'],
+ ['echo', 'exit'],
+ ]);
+ expect(result).toEqual('echo exec, echo exit');
+ });
+
+ it('getProcessExecutableCopyText returns empty string with an invalid array of tuples', () => {
+ // when some sub arrays only have 1 item
+ let result = getProcessExecutableCopyText([['echo', 'exec'], ['echo']]);
+ expect(result).toEqual('');
+
+ // when some sub arrays have more than two item
+ result = getProcessExecutableCopyText([
+ ['echo', 'exec'],
+ ['echo', 'exec', 'random'],
+ ['echo', 'exit'],
+ ]);
+ expect(result).toEqual('');
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.ts
new file mode 100644
index 0000000000000..632e0bc5fd2e3
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.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.
+ */
+
+/**
+ * Serialize an array of executable tuples to a copyable text.
+ *
+ * @param {String[][]} executable
+ * @return {String} serialized string with data of each executable
+ */
+export const getProcessExecutableCopyText = (executable: string[][]) => {
+ try {
+ return executable
+ .map((execTuple) => {
+ const [execCommand, eventAction] = execTuple;
+ if (!execCommand || !eventAction || execTuple.length !== 2) {
+ throw new Error();
+ }
+ return `${execCommand} ${eventAction}`;
+ })
+ .join(', ');
+ } catch (_) {
+ return '';
+ }
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx
new file mode 100644
index 0000000000000..074c69de7e899
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.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 { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { DetailPanelProcess, DetailPanelProcessLeader } from '../../types';
+import { DetailPanelProcessTab } from './index';
+
+const getLeaderDetail = (leader: string): DetailPanelProcessLeader => ({
+ id: `${leader}-id`,
+ name: `${leader}-name`,
+ start: new Date('2022-02-24').toISOString(),
+ entryMetaType: 'sshd',
+ userName: `${leader}-jack`,
+ interactive: true,
+ pid: 1234,
+ entryMetaSourceIp: '10.132.0.50',
+ executable: '/usr/bin/bash',
+});
+
+const TEST_PROCESS_DETAIL: DetailPanelProcess = {
+ id: 'process-id',
+ start: new Date('2022-02-22').toISOString(),
+ end: new Date('2022-02-23').toISOString(),
+ exit_code: 137,
+ user: 'process-jack',
+ args: ['vi', 'test.txt'],
+ executable: [
+ ['test-executable-cmd', '(fork)'],
+ ['test-executable-cmd', '(exec)'],
+ ['test-executable-cmd', '(end)'],
+ ],
+ pid: 1233,
+ entryLeader: getLeaderDetail('entryLeader'),
+ sessionLeader: getLeaderDetail('sessionLeader'),
+ groupLeader: getLeaderDetail('groupLeader'),
+ parent: getLeaderDetail('parent'),
+};
+
+describe('DetailPanelProcessTab component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When DetailPanelProcessTab is mounted', () => {
+ it('renders DetailPanelProcessTab correctly', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ // Process detail rendered correctly
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.id)).toBeVisible();
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.start)).toBeVisible();
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.end)).toBeVisible();
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.exit_code)).toBeVisible();
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.user)).toBeVisible();
+ expect(renderResult.queryByText(`['vi','test.txt']`)).toBeVisible();
+ expect(renderResult.queryAllByText('test-executable-cmd')).toHaveLength(3);
+ expect(renderResult.queryByText('(fork)')).toBeVisible();
+ expect(renderResult.queryByText('(exec)')).toBeVisible();
+ expect(renderResult.queryByText('(end)')).toBeVisible();
+ expect(renderResult.queryByText(TEST_PROCESS_DETAIL.pid)).toBeVisible();
+
+ // Process tab accordions rendered correctly
+ expect(renderResult.queryByText('entryLeader-name')).toBeVisible();
+ expect(renderResult.queryByText('sessionLeader-name')).toBeVisible();
+ expect(renderResult.queryByText('groupLeader-name')).toBeVisible();
+ expect(renderResult.queryByText('parent-name')).toBeVisible();
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx
new file mode 100644
index 0000000000000..97e2cdc806c0f
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx
@@ -0,0 +1,255 @@
+/*
+ * 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, { ReactNode } from 'react';
+import { EuiTextColor } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DetailPanelProcess } from '../../types';
+import { DetailPanelAccordion } from '../detail_panel_accordion';
+import { DetailPanelCopy } from '../detail_panel_copy';
+import { DetailPanelDescriptionList } from '../detail_panel_description_list';
+import { DetailPanelListItem } from '../detail_panel_list_item';
+import { dataOrDash } from '../../utils/data_or_dash';
+import { getProcessExecutableCopyText } from './helpers';
+import { useStyles } from './styles';
+
+interface DetailPanelProcessTabDeps {
+ processDetail: DetailPanelProcess;
+}
+
+type ListItems = Array<{
+ title: NonNullable;
+ description: NonNullable;
+}>;
+
+// TODO: Update placeholder descriptions for these tootips once UX Writer Team Defines them
+const leaderDescriptionListInfo = [
+ {
+ id: 'processEntryLeader',
+ title: 'Entry Leader',
+ tooltipContent: i18n.translate('xpack.sessionView.detailPanel.entryLeaderTooltip', {
+ defaultMessage: 'A entry leader placeholder description',
+ }),
+ },
+ {
+ id: 'processSessionLeader',
+ title: 'Session Leader',
+ tooltipContent: i18n.translate('xpack.sessionView.detailPanel.sessionLeaderTooltip', {
+ defaultMessage: 'A session leader placeholder description',
+ }),
+ },
+ {
+ id: 'processGroupLeader',
+ title: 'Group Leader',
+ tooltipContent: i18n.translate('xpack.sessionView.detailPanel.processGroupLeaderTooltip', {
+ defaultMessage: 'a group leader placeholder description',
+ }),
+ },
+ {
+ id: 'processParent',
+ title: 'Parent',
+ tooltipContent: i18n.translate('xpack.sessionView.detailPanel.processParentTooltip', {
+ defaultMessage: 'a parent placeholder description',
+ }),
+ },
+];
+
+/**
+ * Detail panel in the session view.
+ */
+export const DetailPanelProcessTab = ({ processDetail }: DetailPanelProcessTabDeps) => {
+ const styles = useStyles();
+ const leaderListItems = [
+ processDetail.entryLeader,
+ processDetail.sessionLeader,
+ processDetail.groupLeader,
+ processDetail.parent,
+ ].map((leader, idx) => {
+ const listItems: ListItems = [
+ {
+ title: id,
+ description: (
+
+
+ {dataOrDash(leader.id)}
+
+
+ ),
+ },
+ {
+ title: start,
+ description: (
+
+ {leader.start}
+
+ ),
+ },
+ ];
+ // Only include entry_meta.type for entry leader
+ if (idx === 0) {
+ listItems.push({
+ title: entry_meta.type,
+ description: (
+
+
+ {dataOrDash(leader.entryMetaType)}
+
+
+ ),
+ });
+ }
+ listItems.push(
+ {
+ title: user.name,
+ description: (
+
+ {dataOrDash(leader.userName)}
+
+ ),
+ },
+ {
+ title: interactive,
+ description: (
+
+ {leader.interactive ? 'True' : 'False'}
+
+ ),
+ },
+ {
+ title: pid,
+ description: (
+
+ {dataOrDash(leader.pid)}
+
+ ),
+ }
+ );
+ // Only include entry_meta.source.ip for entry leader
+ if (idx === 0) {
+ listItems.push({
+ title: entry_meta.source.ip,
+ description: (
+
+ {dataOrDash(leader.entryMetaSourceIp)}
+
+ ),
+ });
+ }
+ return {
+ ...leaderDescriptionListInfo[idx],
+ name: leader.name,
+ listItems,
+ };
+ });
+
+ const processArgs = processDetail.args.length
+ ? `[${processDetail.args.map((arg) => `'${arg}'`)}]`
+ : '-';
+
+ return (
+ <>
+ id,
+ description: (
+
+
+ {dataOrDash(processDetail.id)}
+
+
+ ),
+ },
+ {
+ title: start,
+ description: (
+
+ {processDetail.start}
+
+ ),
+ },
+ {
+ title: end,
+ description: (
+
+ {processDetail.end}
+
+ ),
+ },
+ {
+ title: exit_code,
+ description: (
+
+
+ {dataOrDash(processDetail.exit_code)}
+
+
+ ),
+ },
+ {
+ title: user,
+ description: (
+
+ {dataOrDash(processDetail.user)}
+
+ ),
+ },
+ {
+ title: args,
+ description: (
+
+ {processArgs}
+
+ ),
+ },
+ {
+ title: executable,
+ description: (
+
+ {processDetail.executable.map((execTuple, idx) => {
+ const [executable, eventAction] = execTuple;
+ return (
+
+
+ {executable}
+
+
+ {eventAction}
+
+
+ );
+ })}
+
+ ),
+ },
+ {
+ title: process.pid,
+ description: (
+
+
+ {dataOrDash(processDetail.pid)}
+
+
+ ),
+ },
+ ]}
+ />
+ {leaderListItems.map((leader) => (
+
+ ))}
+ >
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts
new file mode 100644
index 0000000000000..8c1154f0c0076
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const description: CSSObject = {
+ width: `calc(100% - ${euiTheme.size.xl})`,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ };
+
+ const descriptionSemibold: CSSObject = {
+ ...description,
+ fontWeight: euiTheme.font.weight.medium,
+ };
+
+ const executableAction: CSSObject = {
+ fontWeight: euiTheme.font.weight.semiBold,
+ paddingLeft: euiTheme.size.xs,
+ };
+
+ return {
+ description,
+ descriptionSemibold,
+ executableAction,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts
new file mode 100644
index 0000000000000..9092009a7d291
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import {
+ mockData,
+ mockProcessMap,
+} from '../../../common/mocks/constants/session_view_process.mock';
+import { Process, ProcessMap } from '../../../common/types/process_tree';
+import {
+ updateProcessMap,
+ buildProcessTree,
+ searchProcessTree,
+ autoExpandProcessTree,
+} from './helpers';
+
+const SESSION_ENTITY_ID = '3d0192c6-7c54-5ee6-a110-3539a7cf42bc';
+const SEARCH_QUERY = 'vi';
+const SEARCH_RESULT_PROCESS_ID = '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727';
+
+const mockEvents = mockData[0].events;
+
+describe('process tree hook helpers tests', () => {
+ let processMap: ProcessMap;
+
+ beforeEach(() => {
+ processMap = {};
+ });
+
+ it('updateProcessMap works', () => {
+ processMap = updateProcessMap(processMap, mockEvents);
+
+ // processes are added to processMap
+ mockEvents.forEach((event) => {
+ expect(processMap[event.process.entity_id]).toBeTruthy();
+ });
+ });
+
+ it('buildProcessTree works', () => {
+ const newOrphans = buildProcessTree(mockProcessMap, mockEvents, [], SESSION_ENTITY_ID);
+
+ const sessionLeaderChildrenIds = new Set(
+ mockProcessMap[SESSION_ENTITY_ID].children.map((child: Process) => child.id)
+ );
+
+ // processes are added under their parent's childrean array in processMap
+ mockEvents.forEach((event) => {
+ expect(sessionLeaderChildrenIds.has(event.process.entity_id));
+ });
+
+ expect(newOrphans.length).toBe(0);
+ });
+
+ it('searchProcessTree works', () => {
+ const searchResults = searchProcessTree(mockProcessMap, SEARCH_QUERY);
+
+ // search returns the process with search query in its event args
+ expect(searchResults[0].id).toBe(SEARCH_RESULT_PROCESS_ID);
+ });
+
+ it('autoExpandProcessTree works', () => {
+ processMap = mockProcessMap;
+ // mock what buildProcessTree does
+ const childProcesses = Object.values(processMap).filter(
+ (process) => process.id !== SESSION_ENTITY_ID
+ );
+ processMap[SESSION_ENTITY_ID].children = childProcesses;
+
+ expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeFalsy();
+ processMap = autoExpandProcessTree(processMap);
+ // session leader should have autoExpand to be true
+ expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts
new file mode 100644
index 0000000000000..d3d7af1c62eda
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts
@@ -0,0 +1,170 @@
+/*
+ * 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 { Process, ProcessEvent, ProcessMap } from '../../../common/types/process_tree';
+import { ProcessImpl } from './hooks';
+
+// given a page of new events, add these events to the appropriate process class model
+// create a new process if none are created and return the mutated processMap
+export const updateProcessMap = (processMap: ProcessMap, events: ProcessEvent[]) => {
+ events.forEach((event) => {
+ const { entity_id: id } = event.process;
+ let process = processMap[id];
+
+ if (!process) {
+ process = new ProcessImpl(id);
+ processMap[id] = process;
+ }
+
+ process.addEvent(event);
+ });
+
+ return processMap;
+};
+
+// given a page of events, update process model parent child relationships
+// if we cannot find a parent for a process include said process
+// in the array of orphans. We track orphans in their own array, so
+// we can attempt to re-parent the orphans when new pages of events are
+// processed. This is especially important when paginating backwards
+// (e.g in the case where the SessionView jumpToEvent prop is used, potentially skipping over ancestor processes)
+export const buildProcessTree = (
+ processMap: ProcessMap,
+ events: ProcessEvent[],
+ orphans: Process[],
+ sessionEntityId: string,
+ backwardDirection: boolean = false
+) => {
+ // we process events in reverse order when paginating backwards.
+ if (backwardDirection) {
+ events = events.slice().reverse();
+ }
+
+ events.forEach((event) => {
+ const process = processMap[event.process.entity_id];
+ const parentProcess = processMap[event.process.parent?.entity_id];
+
+ // if session leader, or process already has a parent, return
+ if (process.id === sessionEntityId || process.parent) {
+ return;
+ }
+
+ if (parentProcess) {
+ process.parent = parentProcess; // handy for recursive operations (like auto expand)
+
+ if (backwardDirection) {
+ parentProcess.children.unshift(process);
+ } else {
+ parentProcess.children.push(process);
+ }
+ } else if (!orphans?.includes(process)) {
+ // if no parent process, process is probably orphaned
+ if (backwardDirection) {
+ orphans?.unshift(process);
+ } else {
+ orphans?.push(process);
+ }
+ }
+ });
+
+ const newOrphans: Process[] = [];
+
+ // with this new page of events processed, lets try re-parent any orphans
+ orphans?.forEach((process) => {
+ const parentProcess = processMap[process.getDetails().process.parent.entity_id];
+
+ if (parentProcess) {
+ process.parent = parentProcess; // handy for recursive operations (like auto expand)
+
+ parentProcess.children.push(process);
+ } else {
+ newOrphans.push(process);
+ }
+ });
+
+ return newOrphans;
+};
+
+// given a plain text searchQuery, iterates over all processes in processMap
+// and marks ones which match the below text (currently what is rendered in the process line item)
+// process.searchMatched is used by process_tree_node to highlight the text which matched the search
+// this funtion also returns a list of process results which is used by session_view_search_bar to drive
+// result navigation UX
+// FYI: this function mutates properties of models contained in processMap
+export const searchProcessTree = (processMap: ProcessMap, searchQuery: string | undefined) => {
+ const results = [];
+
+ for (const processId of Object.keys(processMap)) {
+ const process = processMap[processId];
+
+ if (searchQuery) {
+ const event = process.getDetails();
+ const { working_directory: workingDirectory, args } = event.process;
+
+ // TODO: the text we search is the same as what we render.
+ // in future we may support KQL searches to match against any property
+ // for now plain text search is limited to searching process.working_directory + process.args
+ const text = `${workingDirectory} ${args?.join(' ')}`;
+
+ process.searchMatched = text.includes(searchQuery) ? searchQuery : null;
+
+ if (process.searchMatched) {
+ results.push(process);
+ }
+ } else {
+ process.clearSearch();
+ }
+ }
+
+ return results;
+};
+
+// Iterate over all processes in processMap, and mark each process (and it's ancestors) for auto expansion if:
+// a) the process was "user entered" (aka an interactive group leader)
+// b) matches the plain text search above
+// Returns the processMap with it's processes autoExpand bool set to true or false
+// process.autoExpand is read by process_tree_node to determine whether to auto expand it's child processes.
+export const autoExpandProcessTree = (processMap: ProcessMap) => {
+ for (const processId of Object.keys(processMap)) {
+ const process = processMap[processId];
+
+ if (process.searchMatched || process.isUserEntered()) {
+ let { parent } = process;
+ const parentIdSet = new Set();
+
+ while (parent && !parentIdSet.has(parent.id)) {
+ parentIdSet.add(parent.id);
+ parent.autoExpand = true;
+ parent = parent.parent;
+ }
+ }
+ }
+
+ return processMap;
+};
+
+export const processNewEvents = (
+ eventsProcessMap: ProcessMap,
+ events: ProcessEvent[] | undefined,
+ orphans: Process[],
+ sessionEntityId: string,
+ backwardDirection: boolean = false
+): [ProcessMap, Process[]] => {
+ if (!events || events.length === 0) {
+ return [eventsProcessMap, orphans];
+ }
+
+ const updatedProcessMap = updateProcessMap(eventsProcessMap, events);
+ const newOrphans = buildProcessTree(
+ updatedProcessMap,
+ events,
+ orphans,
+ sessionEntityId,
+ backwardDirection
+ );
+
+ return [autoExpandProcessTree(updatedProcessMap), newOrphans];
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx
new file mode 100644
index 0000000000000..9cece96fe8467
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.test.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 { EventAction } from '../../../common/types/process_tree';
+import { mockEvents } from '../../../common/mocks/constants/session_view_process.mock';
+import { ProcessImpl } from './hooks';
+
+describe('ProcessTree hooks', () => {
+ describe('ProcessImpl.getDetails memoize will cache bust on new events', () => {
+ it('should return the exec event details when this.events changes', () => {
+ const process = new ProcessImpl(mockEvents[0].process.entity_id);
+
+ process.addEvent(mockEvents[0]);
+
+ let result = process.getDetails();
+
+ // push exec event
+ process.addEvent(mockEvents[1]);
+
+ result = process.getDetails();
+
+ expect(result.event.action).toEqual(EventAction.exec);
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts
new file mode 100644
index 0000000000000..a8c6ffe8e75d3
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts
@@ -0,0 +1,255 @@
+/*
+ * 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 _ from 'lodash';
+import memoizeOne from 'memoize-one';
+import { useState, useEffect } from 'react';
+import {
+ EventAction,
+ EventKind,
+ Process,
+ ProcessEvent,
+ ProcessMap,
+ ProcessEventsPage,
+} from '../../../common/types/process_tree';
+import { processNewEvents, searchProcessTree, autoExpandProcessTree } from './helpers';
+import { sortProcesses } from '../../../common/utils/sort_processes';
+
+interface UseProcessTreeDeps {
+ sessionEntityId: string;
+ data: ProcessEventsPage[];
+ searchQuery?: string;
+}
+
+export class ProcessImpl implements Process {
+ id: string;
+ events: ProcessEvent[];
+ children: Process[];
+ parent: Process | undefined;
+ autoExpand: boolean;
+ searchMatched: string | null;
+ orphans: Process[];
+
+ constructor(id: string) {
+ this.id = id;
+ this.events = [];
+ this.children = [];
+ this.orphans = [];
+ this.autoExpand = false;
+ this.searchMatched = null;
+ }
+
+ addEvent(event: ProcessEvent) {
+ // rather than push new events on the array, we return a new one
+ // this helps the below memoizeOne functions to behave correctly.
+ this.events = this.events.concat(event);
+ }
+
+ clearSearch() {
+ this.searchMatched = null;
+ this.autoExpand = false;
+ }
+
+ getChildren(verboseMode: boolean) {
+ let children = this.children;
+
+ // if there are orphans, we just render them inline with the other child processes (currently only session leader does this)
+ if (this.orphans.length) {
+ children = [...children, ...this.orphans].sort(sortProcesses);
+ }
+
+ // When verboseMode is false, we filter out noise via a few techniques.
+ // This option is driven by the "verbose mode" toggle in SessionView/index.tsx
+ if (!verboseMode) {
+ return children.filter((child) => {
+ const { group_leader: groupLeader, session_leader: sessionLeader } =
+ child.getDetails().process;
+
+ // search matches will never be filtered out
+ if (child.searchMatched) {
+ return true;
+ }
+
+ // Hide processes that have their session leader as their process group leader.
+ // This accounts for a lot of noise from bash and other shells forking, running auto completion processes and
+ // other shell startup activities (e.g bashrc .profile etc)
+ if (groupLeader.pid === sessionLeader.pid) {
+ return false;
+ }
+
+ // If the process has no children and has not exec'd (fork only), we hide it.
+ if (child.children.length === 0 && !child.hasExec()) {
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ return children;
+ }
+
+ hasOutput() {
+ return !!this.findEventByAction(this.events, EventAction.output);
+ }
+
+ hasAlerts() {
+ return !!this.findEventByKind(this.events, EventKind.signal);
+ }
+
+ getAlerts() {
+ return this.filterEventsByKind(this.events, EventKind.signal);
+ }
+
+ hasExec() {
+ return !!this.findEventByAction(this.events, EventAction.exec);
+ }
+
+ hasExited() {
+ return !!this.findEventByAction(this.events, EventAction.end);
+ }
+
+ getDetails() {
+ return this.getDetailsMemo(this.events);
+ }
+
+ getOutput() {
+ // not implemented, output ECS schema not defined (for a future release)
+ return '';
+ }
+
+ // isUserEntered is a best guess at which processes were initiated by a real person
+ // In most situations a user entered command in a shell such as bash, will cause bash
+ // to fork, create a new process group, and exec the command (e.g ls). If the session
+ // has a controlling tty (aka an interactive session), we assume process group leaders
+ // with a session leader for a parent are "user entered".
+ // Because of the presence of false positives in this calculation, it is currently
+ // only used to auto expand parts of the tree that could be of interest.
+ isUserEntered() {
+ const event = this.getDetails();
+ const {
+ pid,
+ tty,
+ parent,
+ session_leader: sessionLeader,
+ group_leader: groupLeader,
+ } = event.process;
+
+ const parentIsASessionLeader = parent.pid === sessionLeader.pid; // possibly bash, zsh or some other shell
+ const processIsAGroupLeader = pid === groupLeader.pid;
+ const sessionIsInteractive = !!tty;
+
+ return sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader;
+ }
+
+ getMaxAlertLevel() {
+ // TODO: as part of alerts details work + tie in with the new alert flyout
+ return null;
+ }
+
+ findEventByAction = memoizeOne((events: ProcessEvent[], action: EventAction) => {
+ return events.find(({ event }) => event.action === action);
+ });
+
+ findEventByKind = memoizeOne((events: ProcessEvent[], kind: EventKind) => {
+ return events.find(({ event }) => event.kind === kind);
+ });
+
+ filterEventsByAction = memoizeOne((events: ProcessEvent[], action: EventAction) => {
+ return events.filter(({ event }) => event.action === action);
+ });
+
+ filterEventsByKind = memoizeOne((events: ProcessEvent[], kind: EventKind) => {
+ return events.filter(({ event }) => event.kind === kind);
+ });
+
+ // returns the most recent fork, exec, or end event
+ // to be used as a source for the most up to date details
+ // on the processes lifecycle.
+ getDetailsMemo = memoizeOne((events: ProcessEvent[]) => {
+ const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end];
+ const filtered = events.filter((processEvent) => {
+ return actionsToFind.includes(processEvent.event.action);
+ });
+
+ // because events is already ordered by @timestamp we take the last event
+ // which could be a fork (w no exec or exit), most recent exec event (there can be multiple), or end event.
+ // If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time)
+ return filtered[filtered.length - 1] || ({} as ProcessEvent);
+ });
+}
+
+export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProcessTreeDeps) => {
+ // initialize map, as well as a placeholder for session leader process
+ // we add a fake session leader event, sourced from wide event data.
+ // this is because we might not always have a session leader event
+ // especially if we are paging in reverse from deep within a large session
+ const fakeLeaderEvent = data[0].events.find((event) => event.event.kind === EventKind.event);
+ const sessionLeaderProcess = new ProcessImpl(sessionEntityId);
+
+ if (fakeLeaderEvent) {
+ fakeLeaderEvent.process = {
+ ...fakeLeaderEvent.process,
+ ...fakeLeaderEvent.process.entry_leader,
+ parent: fakeLeaderEvent.process.parent,
+ };
+ sessionLeaderProcess.events.push(fakeLeaderEvent);
+ }
+
+ const initializedProcessMap: ProcessMap = {
+ [sessionEntityId]: sessionLeaderProcess,
+ };
+
+ const [processMap, setProcessMap] = useState(initializedProcessMap);
+ const [processedPages, setProcessedPages] = useState([]);
+ const [searchResults, setSearchResults] = useState([]);
+ const [orphans, setOrphans] = useState([]);
+
+ useEffect(() => {
+ let updatedProcessMap: ProcessMap = processMap;
+ let newOrphans: Process[] = orphans;
+ const newProcessedPages: ProcessEventsPage[] = [];
+
+ data.forEach((page, i) => {
+ const processed = processedPages.find((p) => p.cursor === page.cursor);
+
+ if (!processed) {
+ const backwards = i < processedPages.length;
+
+ const result = processNewEvents(
+ updatedProcessMap,
+ page.events,
+ orphans,
+ sessionEntityId,
+ backwards
+ );
+
+ updatedProcessMap = result[0];
+ newOrphans = result[1];
+
+ newProcessedPages.push(page);
+ }
+ });
+
+ if (newProcessedPages.length > 0) {
+ setProcessMap({ ...updatedProcessMap });
+ setProcessedPages([...processedPages, ...newProcessedPages]);
+ setOrphans(newOrphans);
+ }
+ }, [data, processMap, orphans, processedPages, sessionEntityId]);
+
+ useEffect(() => {
+ setSearchResults(searchProcessTree(processMap, searchQuery));
+ autoExpandProcessTree(processMap);
+ }, [searchQuery, processMap]);
+
+ // set new orphans array on the session leader
+ const sessionLeader = processMap[sessionEntityId];
+
+ sessionLeader.orphans = orphans;
+
+ return { sessionLeader: processMap[sessionEntityId], processMap, searchResults };
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx
new file mode 100644
index 0000000000000..ac6807984ba83
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mockData } from '../../../common/mocks/constants/session_view_process.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { ProcessImpl } from './hooks';
+import { ProcessTree } from './index';
+
+describe('ProcessTree component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When ProcessTree is mounted', () => {
+ it('should render given a valid sessionEntityId and data', () => {
+ renderResult = mockedContext.render(
+ true}
+ hasNextPage={false}
+ fetchPreviousPage={() => true}
+ hasPreviousPage={false}
+ onProcessSelected={jest.fn()}
+ />
+ );
+ expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy();
+ expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy();
+ });
+
+ it('should insert a DOM element used to highlight a process when selectedProcess is set', () => {
+ const mockSelectedProcess = new ProcessImpl(mockData[0].events[0].process.entity_id);
+
+ renderResult = mockedContext.render(
+ true}
+ hasNextPage={false}
+ fetchPreviousPage={() => true}
+ hasPreviousPage={false}
+ selectedProcess={mockSelectedProcess}
+ onProcessSelected={jest.fn()}
+ />
+ );
+
+ // click on view more button
+ renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton').click();
+
+ expect(
+ renderResult
+ .queryByTestId('sessionView:processTreeSelectionArea')
+ ?.parentElement?.getAttribute('data-id')
+ ).toEqual(mockSelectedProcess.id);
+
+ // change the selected process
+ const mockSelectedProcess2 = new ProcessImpl(mockData[0].events[1].process.entity_id);
+
+ renderResult.rerender(
+ true}
+ hasNextPage={false}
+ fetchPreviousPage={() => true}
+ hasPreviousPage={false}
+ selectedProcess={mockSelectedProcess2}
+ onProcessSelected={jest.fn()}
+ />
+ );
+
+ expect(
+ renderResult
+ .queryByTestId('sessionView:processTreeSelectionArea')
+ ?.parentElement?.getAttribute('data-id')
+ ).toEqual(mockSelectedProcess2.id);
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx
new file mode 100644
index 0000000000000..6b3061a0d77bb
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useRef, useEffect, useLayoutEffect, useCallback } from 'react';
+import { EuiButton } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { ProcessTreeNode } from '../process_tree_node';
+import { useProcessTree } from './hooks';
+import { Process, ProcessEventsPage, ProcessEvent } from '../../../common/types/process_tree';
+import { useScroll } from '../../hooks/use_scroll';
+import { useStyles } from './styles';
+
+type FetchFunction = () => void;
+
+interface ProcessTreeDeps {
+ // process.entity_id to act as root node (typically a session (or entry session) leader).
+ sessionEntityId: string;
+
+ data: ProcessEventsPage[];
+
+ jumpToEvent?: ProcessEvent;
+ isFetching: boolean;
+ hasNextPage: boolean | undefined;
+ hasPreviousPage: boolean | undefined;
+ fetchNextPage: FetchFunction;
+ fetchPreviousPage: FetchFunction;
+
+ // plain text search query (only searches "process.working_directory process.args.join(' ')"
+ searchQuery?: string;
+
+ // currently selected process
+ selectedProcess?: Process | null;
+ onProcessSelected: (process: Process) => void;
+ setSearchResults?: (results: Process[]) => void;
+}
+
+export const ProcessTree = ({
+ sessionEntityId,
+ data,
+ jumpToEvent,
+ isFetching,
+ hasNextPage,
+ hasPreviousPage,
+ fetchNextPage,
+ fetchPreviousPage,
+ searchQuery,
+ selectedProcess,
+ onProcessSelected,
+ setSearchResults,
+}: ProcessTreeDeps) => {
+ const styles = useStyles();
+
+ const { sessionLeader, processMap, searchResults } = useProcessTree({
+ sessionEntityId,
+ data,
+ searchQuery,
+ });
+
+ const scrollerRef = useRef(null);
+ const selectionAreaRef = useRef(null);
+
+ useEffect(() => {
+ if (setSearchResults) {
+ setSearchResults(searchResults);
+ }
+ }, [searchResults, setSearchResults]);
+
+ useScroll({
+ div: scrollerRef.current,
+ handler: (pos: number, endReached: boolean) => {
+ if (!isFetching && endReached) {
+ fetchNextPage();
+ }
+ },
+ });
+
+ /**
+ * highlights a process in the tree
+ * we do it this way to avoid state changes on potentially thousands of components
+ */
+ const selectProcess = useCallback(
+ (process: Process) => {
+ if (!selectionAreaRef?.current || !scrollerRef?.current) {
+ return;
+ }
+
+ const selectionAreaEl = selectionAreaRef.current;
+ selectionAreaEl.style.display = 'block';
+
+ // TODO: concept of alert level unknown wrt to elastic security
+ const alertLevel = process.getMaxAlertLevel();
+
+ if (alertLevel && alertLevel >= 0) {
+ selectionAreaEl.style.backgroundColor =
+ alertLevel > 0 ? styles.alertSelected : styles.defaultSelected;
+ } else {
+ selectionAreaEl.style.backgroundColor = '';
+ }
+
+ // find the DOM element for the command which is selected by id
+ const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`);
+
+ if (processEl) {
+ processEl.prepend(selectionAreaEl);
+
+ const cTop = scrollerRef.current.scrollTop;
+ const cBottom = cTop + scrollerRef.current.clientHeight;
+
+ const eTop = processEl.offsetTop;
+ const eBottom = eTop + processEl.clientHeight;
+ const isVisible = eTop >= cTop && eBottom <= cBottom;
+
+ if (!isVisible) {
+ processEl.scrollIntoView({ block: 'center' });
+ }
+ }
+ },
+ [styles.alertSelected, styles.defaultSelected]
+ );
+
+ useLayoutEffect(() => {
+ if (selectedProcess) {
+ selectProcess(selectedProcess);
+ }
+ }, [selectedProcess, selectProcess]);
+
+ useEffect(() => {
+ // after 2 pages are loaded (due to bi-directional jump to), auto select the process
+ // for the jumpToEvent
+ if (jumpToEvent && data.length === 2) {
+ const process = processMap[jumpToEvent.process.entity_id];
+
+ if (process) {
+ onProcessSelected(process);
+ }
+ }
+ }, [jumpToEvent, processMap, onProcessSelected, data]);
+
+ // auto selects the session leader process if no selection is made yet
+ useEffect(() => {
+ if (!selectedProcess) {
+ onProcessSelected(sessionLeader);
+ }
+ }, [sessionLeader, onProcessSelected, selectedProcess]);
+
+ return (
+
+ {hasPreviousPage && (
+
+
+
+ )}
+ {sessionLeader && (
+
+ )}
+
+ {hasNextPage && (
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree/styles.ts b/x-pack/plugins/session_view/public/components/process_tree/styles.ts
new file mode 100644
index 0000000000000..65fb66ad90aa7
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree/styles.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useMemo } from 'react';
+import { transparentize, useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const defaultSelectionColor = euiTheme.colors.accent;
+
+ const scroller: CSSObject = {
+ position: 'relative',
+ fontFamily: euiTheme.font.familyCode,
+ overflow: 'auto',
+ height: '100%',
+ backgroundColor: euiTheme.colors.lightestShade,
+ };
+
+ const selectionArea: CSSObject = {
+ position: 'absolute',
+ display: 'none',
+ marginLeft: '-50%',
+ width: '150%',
+ height: '100%',
+ backgroundColor: defaultSelectionColor,
+ pointerEvents: 'none',
+ opacity: 0.1,
+ };
+
+ const defaultSelected = transparentize(euiTheme.colors.primary, 0.008);
+ const alertSelected = transparentize(euiTheme.colors.danger, 0.008);
+
+ return {
+ scroller,
+ selectionArea,
+ defaultSelected,
+ alertSelected,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx
new file mode 100644
index 0000000000000..618b36578d7da
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { ProcessTreeAlerts } from './index';
+
+describe('ProcessTreeAlerts component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When ProcessTreeAlerts is mounted', () => {
+ it('should return null if no alerts', async () => {
+ renderResult = mockedContext.render();
+
+ expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeNull();
+ });
+
+ it('should return an array of alert details', async () => {
+ renderResult = mockedContext.render();
+
+ expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy();
+ mockAlerts.forEach((alert) => {
+ if (!alert.kibana) {
+ return;
+ }
+ const { uuid, rule, original_event: event, workflow_status: status } = alert.kibana.alert;
+ const { name, query, severity } = rule;
+
+ expect(
+ renderResult.queryByTestId(`sessionView:sessionViewAlertDetail-${uuid}`)
+ ).toBeTruthy();
+ expect(
+ renderResult.queryByTestId(`sessionView:sessionViewAlertDetailViewRule-${uuid}`)
+ ).toBeTruthy();
+ expect(renderResult.queryAllByText(new RegExp(event.action, 'i')).length).toBeTruthy();
+ expect(renderResult.queryAllByText(new RegExp(status, 'i')).length).toBeTruthy();
+ expect(renderResult.queryAllByText(new RegExp(name, 'i')).length).toBeTruthy();
+ expect(renderResult.queryAllByText(new RegExp(query, 'i')).length).toBeTruthy();
+ expect(renderResult.queryAllByText(new RegExp(severity, 'i')).length).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx
new file mode 100644
index 0000000000000..5312c09867b96
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.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 { EuiButton, EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useStyles } from './styles';
+import { ProcessEvent } from '../../../common/types/process_tree';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { CoreStart } from '../../../../../../src/core/public';
+
+interface ProcessTreeAlertsDeps {
+ alerts: ProcessEvent[];
+}
+
+const getRuleUrl = (alert: ProcessEvent, http: CoreStart['http']) => {
+ return http.basePath.prepend(`/app/security/rules/id/${alert.kibana?.alert.rule.uuid}`);
+};
+
+const ProcessTreeAlert = ({ alert }: { alert: ProcessEvent }) => {
+ const { http } = useKibana().services;
+
+ if (!alert.kibana) {
+ return null;
+ }
+
+ const { uuid, rule, original_event: event, workflow_status: status } = alert.kibana.alert;
+ const { name, query, severity } = rule;
+
+ return (
+
+
+
+
+
+
+ {name}
+
+
+
+ {query}
+
+
+
+
+
+ {severity}
+
+
+
+ {status}
+
+
+
+
+
+ {event.action}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export function ProcessTreeAlerts({ alerts }: ProcessTreeAlertsDeps) {
+ const styles = useStyles();
+
+ if (alerts.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {alerts.map((alert: ProcessEvent) => (
+
+ ))}
+
+ );
+}
diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts
new file mode 100644
index 0000000000000..d601891591305
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const { size, colors, border } = euiTheme;
+
+ const container: CSSObject = {
+ marginTop: size.s,
+ marginRight: size.s,
+ color: colors.text,
+ padding: size.m,
+ borderStyle: 'solid',
+ borderColor: colors.lightShade,
+ borderWidth: border.width.thin,
+ borderRadius: border.radius.medium,
+ maxWidth: 800,
+ backgroundColor: 'white',
+ '&>div': {
+ borderTop: border.thin,
+ marginTop: size.m,
+ paddingTop: size.m,
+ '&:first-child': {
+ borderTop: 'none',
+ },
+ },
+ };
+
+ return {
+ container,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx
new file mode 100644
index 0000000000000..16cb946174691
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { EuiButton, EuiIcon, EuiToolTip } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { Process } from '../../../common/types/process_tree';
+import { useButtonStyles } from './use_button_styles';
+
+export const ChildrenProcessesButton = ({
+ onToggle,
+ isExpanded,
+}: {
+ onToggle: () => void;
+ isExpanded: boolean;
+}) => {
+ const { button, buttonArrow, getExpandedIcon } = useButtonStyles();
+
+ return (
+
+
+
+
+ );
+};
+
+export const SessionLeaderButton = ({
+ process,
+ onClick,
+ showGroupLeadersOnly,
+ childCount,
+}: {
+ process: Process;
+ onClick: () => void;
+ showGroupLeadersOnly: boolean;
+ childCount: number;
+}) => {
+ const groupLeaderCount = process.getChildren(false).length;
+ const sameGroupCount = childCount - groupLeaderCount;
+ const { button, buttonArrow, getExpandedIcon } = useButtonStyles();
+
+ if (sameGroupCount > 0) {
+ return (
+
+
+
+ }
+ >
+
+
+
+
+
+ );
+ }
+ return null;
+};
+
+export const AlertButton = ({
+ isExpanded,
+ onToggle,
+}: {
+ isExpanded: boolean;
+ onToggle: () => void;
+}) => {
+ const { alertButton, buttonArrow, getExpandedIcon } = useButtonStyles();
+
+ return (
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
new file mode 100644
index 0000000000000..2a3bf94086021
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
@@ -0,0 +1,200 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import {
+ processMock,
+ childProcessMock,
+ sessionViewAlertProcessMock,
+} from '../../../common/mocks/constants/session_view_process.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { ProcessTreeNode } from './index';
+
+describe('ProcessTreeNode component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When ProcessTreeNode is mounted', () => {
+ it('should render given a valid process', async () => {
+ renderResult = mockedContext.render();
+
+ expect(renderResult.queryByTestId('sessionView:processTreeNode')).toBeTruthy();
+ });
+
+ it('should have an alternate rendering for a session leader', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.container.textContent).toEqual(' bash started by vagrant');
+ });
+
+ // commented out until we get new UX for orphans treatment aka disjointed tree
+ // it('renders orphaned node', async () => {
+ // renderResult = mockedContext.render();
+ // expect(renderResult.queryByText(/orphaned/i)).toBeTruthy();
+ // });
+
+ it('renders Exec icon and exit code for executed process', async () => {
+ const executedProcessMock: typeof processMock = {
+ ...processMock,
+ hasExec: () => true,
+ };
+
+ renderResult = mockedContext.render();
+
+ expect(renderResult.queryByTestId('sessionView:processTreeNodeExecIcon')).toBeTruthy();
+ expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeTruthy();
+ });
+
+ it('does not render exit code if it does not exist', async () => {
+ const processWithoutExitCode: typeof processMock = {
+ ...processMock,
+ hasExec: () => true,
+ getDetails: () => ({
+ ...processMock.getDetails(),
+ process: {
+ ...processMock.getDetails().process,
+ exit_code: undefined,
+ },
+ }),
+ };
+
+ renderResult = mockedContext.render();
+ expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeFalsy();
+ });
+
+ it('renders Root Escalation flag properly', async () => {
+ const rootEscalationProcessMock: typeof processMock = {
+ ...processMock,
+ getDetails: () => ({
+ ...processMock.getDetails(),
+ user: {
+ id: '-1',
+ name: 'root',
+ },
+ process: {
+ ...processMock.getDetails().process,
+ parent: {
+ ...processMock.getDetails().process.parent,
+ user: {
+ name: 'test',
+ id: '1000',
+ },
+ },
+ },
+ }),
+ };
+
+ renderResult = mockedContext.render();
+
+ expect(
+ renderResult.queryByTestId('sessionView:processTreeNodeRootEscalationFlag')
+ ).toBeTruthy();
+ });
+
+ it('executes callback function when user Clicks', async () => {
+ const onProcessSelected = jest.fn();
+
+ renderResult = mockedContext.render(
+
+ );
+
+ userEvent.click(renderResult.getByTestId('sessionView:processTreeNodeRow'));
+ expect(onProcessSelected).toHaveBeenCalled();
+ });
+
+ it('does not executes callback function when user is Clicking to copy text', async () => {
+ const windowGetSelectionSpy = jest.spyOn(window, 'getSelection');
+
+ const onProcessSelected = jest.fn();
+
+ renderResult = mockedContext.render(
+
+ );
+
+ // @ts-ignore
+ windowGetSelectionSpy.mockImplementation(() => ({ type: 'Range' }));
+
+ userEvent.click(renderResult.getByTestId('sessionView:processTreeNodeRow'));
+ expect(onProcessSelected).not.toHaveBeenCalled();
+
+ // cleanup
+ windowGetSelectionSpy.mockRestore();
+ });
+ describe('Alerts', () => {
+ it('renders Alert button when process has alerts', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ expect(renderResult.queryByTestId('processTreeNodeAlertButton')).toBeTruthy();
+ });
+ it('toggle Alert Details button when Alert button is clicked', async () => {
+ renderResult = mockedContext.render(
+
+ );
+ userEvent.click(renderResult.getByTestId('processTreeNodeAlertButton'));
+ expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy();
+ userEvent.click(renderResult.getByTestId('processTreeNodeAlertButton'));
+ expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeFalsy();
+ });
+ });
+ describe('Child processes', () => {
+ it('renders Child processes button when process has Child processes', async () => {
+ const processMockWithChildren: typeof processMock = {
+ ...processMock,
+ getChildren: () => [childProcessMock],
+ };
+
+ renderResult = mockedContext.render();
+
+ expect(
+ renderResult.queryByTestId('sessionView:processTreeNodeChildProcessesButton')
+ ).toBeTruthy();
+ });
+ it('toggle Child processes nodes when Child processes button is clicked', async () => {
+ const processMockWithChildren: typeof processMock = {
+ ...processMock,
+ getChildren: () => [childProcessMock],
+ };
+
+ renderResult = mockedContext.render();
+
+ expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(1);
+
+ userEvent.click(
+ renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton')
+ );
+ expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(2);
+
+ userEvent.click(
+ renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton')
+ );
+ expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(1);
+ });
+ });
+ describe('Search', () => {
+ it('highlights text within the process node line item if it matches the searchQuery', () => {
+ // set a mock search matched indicator for the process (typically done by ProcessTree/helpers.ts)
+ processMock.searchMatched = '/vagrant';
+
+ renderResult = mockedContext.render();
+
+ expect(
+ renderResult.getByTestId('sessionView:processNodeSearchHighlight').textContent
+ ).toEqual('/vagrant');
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
new file mode 100644
index 0000000000000..9db83f58f7738
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
@@ -0,0 +1,213 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ *2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, {
+ useRef,
+ useLayoutEffect,
+ useState,
+ useEffect,
+ MouseEvent,
+ useCallback,
+} from 'react';
+import { EuiButton, EuiIcon } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { Process } from '../../../common/types/process_tree';
+import { useStyles } from './styles';
+import { ProcessTreeAlerts } from '../process_tree_alerts';
+import { SessionLeaderButton, AlertButton, ChildrenProcessesButton } from './buttons';
+import { useButtonStyles } from './use_button_styles';
+interface ProcessDeps {
+ process: Process;
+ isSessionLeader?: boolean;
+ depth?: number;
+ onProcessSelected?: (process: Process) => void;
+}
+
+/**
+ * Renders a node on the process tree
+ */
+export function ProcessTreeNode({
+ process,
+ isSessionLeader = false,
+ depth = 0,
+ onProcessSelected,
+}: ProcessDeps) {
+ const textRef = useRef(null);
+
+ const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand);
+ const [alertsExpanded, setAlertsExpanded] = useState(false);
+ const [showGroupLeadersOnly, setShowGroupLeadersOnly] = useState(isSessionLeader);
+ const { searchMatched } = process;
+
+ useEffect(() => {
+ setChildrenExpanded(isSessionLeader || process.autoExpand);
+ }, [isSessionLeader, process.autoExpand]);
+
+ const alerts = process.getAlerts();
+ const styles = useStyles({ depth, hasAlerts: !!alerts.length });
+ const buttonStyles = useButtonStyles();
+
+ useLayoutEffect(() => {
+ if (searchMatched !== null && textRef.current) {
+ const regex = new RegExp(searchMatched);
+ const text = textRef.current.textContent;
+
+ if (text) {
+ const html = text.replace(regex, (match) => {
+ return `${match}`;
+ });
+
+ // eslint-disable-next-line no-unsanitized/property
+ textRef.current.innerHTML = html;
+ }
+ }
+ }, [searchMatched, styles.searchHighlight]);
+
+ const onShowGroupLeaderOnlyClick = useCallback(() => {
+ setShowGroupLeadersOnly(!showGroupLeadersOnly);
+ }, [showGroupLeadersOnly]);
+
+ const onChildrenToggle = useCallback(() => {
+ setChildrenExpanded(!childrenExpanded);
+ }, [childrenExpanded]);
+
+ const onAlertsToggle = useCallback(() => {
+ setAlertsExpanded(!alertsExpanded);
+ }, [alertsExpanded]);
+
+ const onProcessClicked = (e: MouseEvent) => {
+ e.stopPropagation();
+
+ const selection = window.getSelection();
+
+ // do not select the command if the user was just selecting text for copy.
+ if (selection && selection.type === 'Range') {
+ return;
+ }
+
+ onProcessSelected?.(process);
+ };
+
+ const processDetails = process.getDetails();
+
+ if (!processDetails) {
+ return null;
+ }
+
+ const id = process.id;
+ const { user } = processDetails;
+ const {
+ args,
+ name,
+ tty,
+ parent,
+ working_directory: workingDirectory,
+ exit_code: exitCode,
+ } = processDetails.process;
+
+ const children = process.getChildren(!showGroupLeadersOnly);
+ const childCount = process.getChildren(true).length;
+ const shouldRenderChildren = childrenExpanded && children && children.length > 0;
+ const childrenTreeDepth = depth + 1;
+
+ const showRootEscalation = user.name === 'root' && user.id !== parent.user.id;
+ const interactiveSession = !!tty;
+ const sessionIcon = interactiveSession ? 'consoleApp' : 'compute';
+ const hasExec = process.hasExec();
+ const iconTestSubj = hasExec
+ ? 'sessionView:processTreeNodeExecIcon'
+ : 'sessionView:processTreeNodeForkIcon';
+ const processIcon = hasExec ? 'console' : 'branch';
+
+ return (
+
+
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
+
+ {isSessionLeader ? (
+ <>
+
{name || args[0]}{' '}
+
{' '}
+
{user.name}
+
+ >
+ ) : (
+
+
+
+ {workingDirectory}
+ {args[0]}
+ {args.slice(1).join(' ')}
+ {exitCode !== undefined && (
+
+ {' '}
+ [exit_code: {exitCode}]
+
+ )}
+
+
+ )}
+
+ {showRootEscalation && (
+
+
+
+ )}
+ {!isSessionLeader && childCount > 0 && (
+
+ )}
+ {alerts.length > 0 && (
+
+ )}
+
+
+
+ {alertsExpanded &&
}
+
+ {shouldRenderChildren && (
+
+ {children.map((child) => {
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
new file mode 100644
index 0000000000000..07092d6de28ea
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 { useMemo } from 'react';
+import { useEuiTheme, transparentize } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+interface StylesDeps {
+ depth: number;
+ hasAlerts: boolean;
+}
+
+export const useStyles = ({ depth, hasAlerts }: StylesDeps) => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const { colors, border, size } = euiTheme;
+
+ const TREE_INDENT = euiTheme.base * 2;
+
+ const darkText: CSSObject = {
+ color: colors.text,
+ };
+
+ const searchHighlight = `
+ background-color: ${colors.highlight};
+ color: ${colors.fullShade};
+ border-radius: ${border.radius.medium};
+ `;
+
+ const children: CSSObject = {
+ position: 'relative',
+ color: colors.ghost,
+ marginLeft: size.base,
+ paddingLeft: size.s,
+ borderLeft: border.editable,
+ marginTop: size.s,
+ };
+
+ /**
+ * gets border, bg and hover colors for a process
+ */
+ const getHighlightColors = () => {
+ let bgColor = 'none';
+ const hoverColor = transparentize(colors.primary, 0.04);
+ let borderColor = 'transparent';
+
+ // TODO: alerts highlight colors
+ if (hasAlerts) {
+ bgColor = transparentize(colors.danger, 0.04);
+ borderColor = transparentize(colors.danger, 0.48);
+ }
+
+ return { bgColor, borderColor, hoverColor };
+ };
+
+ const { bgColor, borderColor, hoverColor } = getHighlightColors();
+
+ const processNode: CSSObject = {
+ display: 'block',
+ cursor: 'pointer',
+ position: 'relative',
+ margin: `${size.s} 0px`,
+ '&:not(:first-child)': {
+ marginTop: size.s,
+ },
+ '&:hover:before': {
+ backgroundColor: hoverColor,
+ },
+ '&:before': {
+ position: 'absolute',
+ height: '100%',
+ pointerEvents: 'none',
+ content: `''`,
+ marginLeft: `-${depth * TREE_INDENT}px`,
+ borderLeft: `${size.xs} solid ${borderColor}`,
+ backgroundColor: bgColor,
+ width: `calc(100% + ${depth * TREE_INDENT}px)`,
+ },
+ };
+
+ const wrapper: CSSObject = {
+ paddingLeft: size.s,
+ position: 'relative',
+ verticalAlign: 'middle',
+ color: colors.mediumShade,
+ wordBreak: 'break-all',
+ minHeight: size.l,
+ lineHeight: size.l,
+ };
+
+ const workingDir: CSSObject = {
+ color: colors.successText,
+ };
+
+ const alertDetails: CSSObject = {
+ padding: size.s,
+ border: border.editable,
+ borderRadius: border.radius.medium,
+ };
+
+ return {
+ darkText,
+ searchHighlight,
+ children,
+ processNode,
+ wrapper,
+ workingDir,
+ alertDetails,
+ };
+ }, [depth, euiTheme, hasAlerts]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts
new file mode 100644
index 0000000000000..d208fa8f079af
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.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 { useMemo } from 'react';
+import { useEuiTheme, transparentize } from '@elastic/eui';
+import { euiLightVars as theme } from '@kbn/ui-theme';
+import { CSSObject } from '@emotion/react';
+
+export const useButtonStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const { colors, border, font, size } = euiTheme;
+
+ const button: CSSObject = {
+ background: transparentize(theme.euiColorVis6, 0.04),
+ border: `${border.width.thin} solid ${transparentize(theme.euiColorVis6, 0.48)}`,
+ lineHeight: '18px',
+ height: '20px',
+ fontSize: '11px',
+ fontFamily: font.familyCode,
+ borderRadius: border.radius.medium,
+ color: colors.text,
+ marginLeft: size.s,
+ minWidth: 0,
+ };
+
+ const buttonArrow: CSSObject = {
+ marginLeft: size.s,
+ };
+
+ const alertButton: CSSObject = {
+ ...button,
+ background: transparentize(colors.dangerText, 0.04),
+ border: `${border.width.thin} solid ${transparentize(colors.dangerText, 0.48)}`,
+ };
+
+ const userChangedButton: CSSObject = {
+ ...button,
+ background: transparentize(theme.euiColorVis1, 0.04),
+ border: `${border.width.thin} solid ${transparentize(theme.euiColorVis1, 0.48)}`,
+ };
+
+ const getExpandedIcon = (expanded: boolean) => {
+ return expanded ? 'arrowUp' : 'arrowDown';
+ };
+
+ return {
+ buttonArrow,
+ button,
+ alertButton,
+ userChangedButton,
+ getExpandedIcon,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts
new file mode 100644
index 0000000000000..b93e5b43ddf88
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { useEffect, useState } from 'react';
+import { useInfiniteQuery } from 'react-query';
+import { EuiSearchBarOnChangeArgs } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { ProcessEvent, ProcessEventResults } from '../../../common/types/process_tree';
+import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../../common/constants';
+
+export const useFetchSessionViewProcessEvents = (
+ sessionEntityId: string,
+ jumpToEvent: ProcessEvent | undefined
+) => {
+ const { http } = useKibana().services;
+
+ const jumpToCursor = jumpToEvent && jumpToEvent['@timestamp'];
+
+ const query = useInfiniteQuery(
+ 'sessionViewProcessEvents',
+ async ({ pageParam = {} }) => {
+ let { cursor } = pageParam;
+ const { forward } = pageParam;
+
+ if (!cursor && jumpToCursor) {
+ cursor = jumpToCursor;
+ }
+
+ const res = await http.get(PROCESS_EVENTS_ROUTE, {
+ query: {
+ sessionEntityId,
+ cursor,
+ forward,
+ },
+ });
+
+ const events = res.events.map((event: any) => event._source as ProcessEvent);
+
+ return { events, cursor };
+ },
+ {
+ getNextPageParam: (lastPage, pages) => {
+ if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) {
+ return {
+ cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'],
+ forward: true,
+ };
+ }
+ },
+ getPreviousPageParam: (firstPage, pages) => {
+ if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) {
+ return {
+ cursor: firstPage.events[0]['@timestamp'],
+ forward: false,
+ };
+ }
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ }
+ );
+
+ useEffect(() => {
+ if (jumpToEvent && query.data?.pages.length === 1) {
+ query.fetchPreviousPage();
+ }
+ }, [jumpToEvent, query]);
+
+ return query;
+};
+
+export const useSearchQuery = () => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => {
+ if (query) {
+ setSearchQuery(query.text);
+ } else {
+ setSearchQuery('');
+ }
+ };
+
+ return {
+ searchQuery,
+ onSearch,
+ };
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view/index.test.tsx
new file mode 100644
index 0000000000000..41336977cf78a
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view/index.test.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 { waitFor, waitForElementToBeRemoved } from '@testing-library/react';
+import React from 'react';
+import { sessionViewProcessEventsMock } from '../../../common/mocks/responses/session_view_process_events.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { SessionView } from './index';
+import userEvent from '@testing-library/user-event';
+
+describe('SessionView component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+ let mockedApi: AppContextTestRender['coreStart']['http']['get'];
+
+ const waitForApiCall = () => waitFor(() => expect(mockedApi).toHaveBeenCalled());
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ mockedApi = mockedContext.coreStart.http.get;
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ describe('When SessionView is mounted', () => {
+ describe('And no data exists', () => {
+ beforeEach(async () => {
+ mockedApi.mockResolvedValue({
+ events: [],
+ });
+ });
+
+ it('should show the Empty message', async () => {
+ render();
+ await waitForApiCall();
+ expect(renderResult.getByTestId('sessionView:sessionViewProcessEventsEmpty')).toBeTruthy();
+ });
+
+ it('should not display the search bar', async () => {
+ render();
+ await waitForApiCall();
+ expect(
+ renderResult.queryByTestId('sessionView:sessionViewProcessEventsSearch')
+ ).toBeFalsy();
+ });
+ });
+
+ describe('And data exists', () => {
+ beforeEach(async () => {
+ mockedApi.mockResolvedValue(sessionViewProcessEventsMock);
+ });
+
+ it('should show loading indicator while retrieving data and hide it when it gets it', async () => {
+ let releaseApiResponse: (value?: unknown) => void;
+
+ // make the request wait
+ mockedApi.mockReturnValue(new Promise((resolve) => (releaseApiResponse = resolve)));
+ render();
+ await waitForApiCall();
+
+ // see if loader is present
+ expect(renderResult.getByText('Loading session…')).toBeTruthy();
+
+ // release the request
+ releaseApiResponse!(mockedApi);
+
+ // check the loader is gone
+ await waitForElementToBeRemoved(renderResult.getByText('Loading session…'));
+ });
+
+ it('should display the search bar', async () => {
+ render();
+ await waitForApiCall();
+ expect(renderResult.getByTestId('sessionView:sessionViewProcessEventsSearch')).toBeTruthy();
+ });
+
+ it('should show items on the list, and auto selects session leader', async () => {
+ render();
+ await waitForApiCall();
+
+ expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toBeTruthy();
+
+ const selectionArea = renderResult.queryByTestId('sessionView:processTreeSelectionArea');
+
+ expect(selectionArea?.parentElement?.getAttribute('data-id')).toEqual('test-entity-id');
+ });
+
+ it('should toggle detail panel visibilty when detail button clicked', async () => {
+ render();
+ await waitForApiCall();
+
+ userEvent.click(renderResult.getByTestId('sessionViewDetailPanelToggle'));
+ expect(renderResult.getByText('Process')).toBeTruthy();
+ expect(renderResult.getByText('Host')).toBeTruthy();
+ expect(renderResult.getByText('Alerts')).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx
new file mode 100644
index 0000000000000..7a82edc94ff1b
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx
@@ -0,0 +1,205 @@
+/*
+ * 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, useCallback } from 'react';
+import {
+ EuiEmptyPrompt,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiResizableContainer,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { SectionLoading } from '../../shared_imports';
+import { ProcessTree } from '../process_tree';
+import { Process, ProcessEvent } from '../../../common/types/process_tree';
+import { SessionViewDetailPanel } from '../session_view_detail_panel';
+import { SessionViewSearchBar } from '../session_view_search_bar';
+import { useStyles } from './styles';
+import { useFetchSessionViewProcessEvents } from './hooks';
+
+interface SessionViewDeps {
+ // the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id
+ sessionEntityId: string;
+ height?: number;
+ jumpToEvent?: ProcessEvent;
+}
+
+/**
+ * The main wrapper component for the session view.
+ */
+export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionViewDeps) => {
+ const [isDetailOpen, setIsDetailOpen] = useState(false);
+ const [selectedProcess, setSelectedProcess] = useState(null);
+
+ const styles = useStyles({ height });
+
+ const onProcessSelected = useCallback((process: Process) => {
+ setSelectedProcess(process);
+ }, []);
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState(null);
+
+ const {
+ data,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetching,
+ fetchPreviousPage,
+ hasPreviousPage,
+ } = useFetchSessionViewProcessEvents(sessionEntityId, jumpToEvent);
+
+ const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0;
+ const renderIsLoading = isFetching && !data;
+ const renderDetails = isDetailOpen && selectedProcess;
+ const toggleDetailPanel = () => {
+ setIsDetailOpen(!isDetailOpen);
+ };
+
+ if (!isFetching && !hasData) {
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {(EuiResizablePanel, EuiResizableButton) => (
+ <>
+
+ {renderIsLoading && (
+
+
+
+ )}
+
+ {error && (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ )}
+
+ {hasData && (
+
+ )}
+
+
+ {renderDetails ? (
+ <>
+
+
+
+
+ >
+ ) : (
+ <>
+ {/* Returning an empty element here (instead of false) to avoid a bug in EuiResizableContainer */}
+ >
+ )}
+ >
+ )}
+
+ >
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { SessionView as default };
diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts
new file mode 100644
index 0000000000000..d7159ec5b1b39
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view/styles.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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+interface StylesDeps {
+ height: number | undefined;
+}
+
+export const useStyles = ({ height = 500 }: StylesDeps) => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const processTree: CSSObject = {
+ height: `${height}px`,
+ paddingTop: euiTheme.size.s,
+ };
+
+ const detailPanel: CSSObject = {
+ height: `${height}px`,
+ };
+
+ return {
+ processTree,
+ detailPanel,
+ };
+ }, [height, euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts
new file mode 100644
index 0000000000000..295371fbff96c
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { Process, ProcessFields } from '../../../common/types/process_tree';
+import { DetailPanelProcess, EuiTabProps } from '../../types';
+
+const getDetailPanelProcessLeader = (leader: ProcessFields) => ({
+ ...leader,
+ id: leader.entity_id,
+ entryMetaType: leader.entry_meta?.type || '',
+ userName: leader.user.name,
+ entryMetaSourceIp: leader.entry_meta?.source.ip || '',
+});
+
+export const getDetailPanelProcess = (process: Process) => {
+ const processData = {} as DetailPanelProcess;
+
+ processData.id = process.id;
+ processData.start = process.events[0]['@timestamp'];
+ processData.end = process.events[process.events.length - 1]['@timestamp'];
+ const args = new Set();
+ processData.executable = [];
+
+ process.events.forEach((event) => {
+ if (!processData.user) {
+ processData.user = event.user.name;
+ }
+ if (!processData.pid) {
+ processData.pid = event.process.pid;
+ }
+
+ if (event.process.args.length > 0) {
+ args.add(event.process.args.join(' '));
+ }
+ if (event.process.executable) {
+ processData.executable.push([event.process.executable, `(${event.event.action})`]);
+ }
+ if (event.process.exit_code) {
+ processData.exit_code = event.process.exit_code;
+ }
+ });
+
+ processData.args = [...args];
+ processData.entryLeader = getDetailPanelProcessLeader(process.events[0].process.entry_leader);
+ processData.sessionLeader = getDetailPanelProcessLeader(process.events[0].process.session_leader);
+ processData.groupLeader = getDetailPanelProcessLeader(process.events[0].process.group_leader);
+ processData.parent = getDetailPanelProcessLeader(process.events[0].process.parent);
+
+ return processData;
+};
+
+export const getSelectedTabContent = (tabs: EuiTabProps[], selectedTabId: string) => {
+ const selectedTab = tabs.find((tab) => tab.id === selectedTabId);
+
+ if (selectedTab) {
+ return selectedTab.content;
+ }
+
+ return null;
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx
new file mode 100644
index 0000000000000..f754086fe5fab
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx
@@ -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 React from 'react';
+import { sessionViewBasicProcessMock } from '../../../common/mocks/constants/session_view_process.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { SessionViewDetailPanel } from './index';
+
+describe('SessionView component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ describe('When SessionViewDetailPanel is mounted', () => {
+ it('shows process detail by default', async () => {
+ renderResult = mockedContext.render(
+
+ );
+ expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible();
+ });
+
+ it('can switch tabs to show host details', async () => {
+ renderResult = mockedContext.render(
+
+ );
+
+ renderResult.queryByText('Host')?.click();
+ expect(renderResult.queryByText('hostname')).toBeVisible();
+ expect(renderResult.queryAllByText('james-fleet-714-2')).toHaveLength(2);
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx
new file mode 100644
index 0000000000000..a47ce1d91ac97
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useState, useMemo, useCallback } from 'react';
+import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui';
+import { EuiTabProps } from '../../types';
+import { Process } from '../../../common/types/process_tree';
+import { getDetailPanelProcess, getSelectedTabContent } from './helpers';
+import { DetailPanelProcessTab } from '../detail_panel_process_tab';
+import { DetailPanelHostTab } from '../detail_panel_host_tab';
+
+interface SessionViewDetailPanelDeps {
+ selectedProcess: Process;
+ onProcessSelected?: (process: Process) => void;
+}
+
+/**
+ * Detail panel in the session view.
+ */
+export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPanelDeps) => {
+ const [selectedTabId, setSelectedTabId] = useState('process');
+ const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]);
+
+ const tabs: EuiTabProps[] = useMemo(
+ () => [
+ {
+ id: 'process',
+ name: 'Process',
+ content: ,
+ },
+ {
+ id: 'host',
+ name: 'Host',
+ content: ,
+ },
+ {
+ id: 'alerts',
+ disabled: true,
+ name: 'Alerts',
+ append: (
+
+ 10
+
+ ),
+ content: null,
+ },
+ ],
+ [processDetail, selectedProcess.events]
+ );
+
+ const onSelectedTabChanged = useCallback((id: string) => {
+ setSelectedTabId(id);
+ }, []);
+
+ const tabContent = useMemo(
+ () => getSelectedTabContent(tabs, selectedTabId),
+ [tabs, selectedTabId]
+ );
+
+ return (
+ <>
+
+ {tabs.map((tab, index) => (
+ onSelectedTabChanged(tab.id)}
+ isSelected={tab.id === selectedTabId}
+ disabled={tab.disabled}
+ prepend={tab.prepend}
+ append={tab.append}
+ >
+ {tab.name}
+
+ ))}
+
+ {tabContent}
+ >
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx
new file mode 100644
index 0000000000000..b27260668af07
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.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 { processMock } from '../../../common/mocks/constants/session_view_process.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
+import { SessionViewSearchBar } from './index';
+import userEvent from '@testing-library/user-event';
+import { fireEvent } from '@testing-library/dom';
+
+describe('SessionViewSearchBar component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let mockedContext: AppContextTestRender;
+
+ beforeEach(() => {
+ mockedContext = createAppRootMockRenderer();
+ });
+
+ it('handles a typed search query', async () => {
+ const mockSetSearchQuery = jest.fn((query) => query);
+ const mockOnProcessSelected = jest.fn((process) => process);
+
+ renderResult = mockedContext.render(
+
+ );
+
+ const searchInput = renderResult.getByTestId('sessionView:searchInput').querySelector('input');
+
+ expect(searchInput?.value).toEqual('ls');
+
+ if (searchInput) {
+ userEvent.type(searchInput, ' -la');
+ fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' });
+ }
+
+ expect(searchInput?.value).toEqual('ls -la');
+ expect(mockSetSearchQuery.mock.calls.length).toBe(1);
+ expect(mockSetSearchQuery.mock.results[0].value).toBe('ls -la');
+ });
+
+ it('shows a results navigator when searchResults provided', async () => {
+ const processMock2 = { ...processMock };
+ const processMock3 = { ...processMock };
+ const mockResults = [processMock, processMock2, processMock3];
+ const mockSetSearchQuery = jest.fn((query) => query);
+ const mockOnProcessSelected = jest.fn((process) => process);
+
+ renderResult = mockedContext.render(
+
+ );
+
+ const searchPagination = renderResult.getByTestId('sessionView:searchPagination');
+ expect(searchPagination).toBeTruthy();
+
+ const paginationTextClass = '.euiPagination__compressedText';
+ expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('1 of 3');
+
+ userEvent.click(renderResult.getByTestId('pagination-button-next'));
+ expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('2 of 3');
+
+ const searchInput = renderResult.getByTestId('sessionView:searchInput').querySelector('input');
+
+ if (searchInput) {
+ userEvent.type(searchInput, ' -la');
+ fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' });
+ }
+
+ // after search is changed, results index should reset to 1
+ expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('1 of 3');
+
+ // setSelectedProcess should be called 3 times:
+ // 1. searchResults is set so auto select first item
+ // 2. next button hit, so call with 2nd item
+ // 3. search changed, so call with first result.
+ expect(mockOnProcessSelected.mock.calls.length).toBe(3);
+ expect(mockOnProcessSelected.mock.results[0].value).toEqual(processMock);
+ expect(mockOnProcessSelected.mock.results[1].value).toEqual(processMock2);
+ expect(mockOnProcessSelected.mock.results[1].value).toEqual(processMock);
+ });
+});
diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx
new file mode 100644
index 0000000000000..f4e4dac7a94c7
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useState, useEffect } from 'react';
+import { EuiSearchBar, EuiPagination } from '@elastic/eui';
+import { EuiSearchBarOnChangeArgs } from '@elastic/eui';
+import { Process } from '../../../common/types/process_tree';
+import { useStyles } from './styles';
+
+interface SessionViewSearchBarDeps {
+ searchQuery: string;
+ setSearchQuery(val: string): void;
+ searchResults: Process[] | null;
+ onProcessSelected(process: Process): void;
+}
+
+/**
+ * The main wrapper component for the session view.
+ */
+export const SessionViewSearchBar = ({
+ searchQuery,
+ setSearchQuery,
+ onProcessSelected,
+ searchResults,
+}: SessionViewSearchBarDeps) => {
+ const styles = useStyles();
+
+ const [selectedResult, setSelectedResult] = useState(0);
+
+ const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => {
+ setSelectedResult(0);
+
+ if (query) {
+ setSearchQuery(query.text);
+ } else {
+ setSearchQuery('');
+ }
+ };
+
+ useEffect(() => {
+ if (searchResults) {
+ const process = searchResults[selectedResult];
+
+ if (process) {
+ onProcessSelected(process);
+ }
+ }
+ }, [searchResults, onProcessSelected, selectedResult]);
+
+ const showPagination = !!searchResults?.length;
+
+ return (
+
+
+ {showPagination && (
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.ts b/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.ts
new file mode 100644
index 0000000000000..97a49ca2aa8c1
--- /dev/null
+++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.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 { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
+import { CSSObject } from '@emotion/react';
+
+export const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+
+ const cached = useMemo(() => {
+ const pagination: CSSObject = {
+ position: 'absolute',
+ top: euiTheme.size.s,
+ right: euiTheme.size.xxl,
+ };
+
+ return {
+ pagination,
+ };
+ }, [euiTheme]);
+
+ return cached;
+};
diff --git a/x-pack/plugins/session_view/public/hooks/use_scroll.ts b/x-pack/plugins/session_view/public/hooks/use_scroll.ts
new file mode 100644
index 0000000000000..716e35dbb0987
--- /dev/null
+++ b/x-pack/plugins/session_view/public/hooks/use_scroll.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 { useEffect } from 'react';
+import _ from 'lodash';
+
+const SCROLL_END_BUFFER_HEIGHT = 20;
+const DEBOUNCE_TIMEOUT = 500;
+
+function getScrollPosition(div: HTMLElement) {
+ if (div) {
+ return div.scrollTop;
+ } else {
+ return document.documentElement.scrollTop || document.body.scrollTop;
+ }
+}
+
+interface IUseScrollDeps {
+ div: HTMLElement | null;
+ handler(pos: number, endReached: boolean): void;
+}
+
+/**
+ * listens to scroll events on given div, if scroll reaches bottom, calls a callback
+ * @param {ref} ref to listen to scroll events on
+ * @param {function} handler function receives params (scrollTop, endReached)
+ */
+export function useScroll({ div, handler }: IUseScrollDeps) {
+ useEffect(() => {
+ if (div) {
+ const debounced = _.debounce(() => {
+ const pos = getScrollPosition(div);
+ const endReached = pos + div.offsetHeight > div.scrollHeight - SCROLL_END_BUFFER_HEIGHT;
+
+ handler(pos, endReached);
+ }, DEBOUNCE_TIMEOUT);
+
+ div.onscroll = debounced;
+
+ return () => {
+ debounced.cancel();
+
+ div.onscroll = null;
+ };
+ }
+ }, [div, handler]);
+}
diff --git a/x-pack/plugins/session_view/public/index.ts b/x-pack/plugins/session_view/public/index.ts
new file mode 100644
index 0000000000000..90043e9a691dc
--- /dev/null
+++ b/x-pack/plugins/session_view/public/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SessionViewPlugin } from './plugin';
+
+export function plugin() {
+ return new SessionViewPlugin();
+}
diff --git a/x-pack/plugins/session_view/public/methods/index.tsx b/x-pack/plugins/session_view/public/methods/index.tsx
new file mode 100644
index 0000000000000..560bb302ebabf
--- /dev/null
+++ b/x-pack/plugins/session_view/public/methods/index.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { lazy, Suspense } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { QueryClient, QueryClientProvider } from 'react-query';
+
+// Initializing react-query
+const queryClient = new QueryClient();
+
+const SessionViewLazy = lazy(() => import('../components/session_view'));
+
+export const getSessionViewLazy = (sessionEntityId: string) => {
+ return (
+
+ }>
+
+
+
+ );
+};
diff --git a/x-pack/plugins/session_view/public/plugin.ts b/x-pack/plugins/session_view/public/plugin.ts
new file mode 100644
index 0000000000000..d25c95b00b2c6
--- /dev/null
+++ b/x-pack/plugins/session_view/public/plugin.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CoreSetup, CoreStart, Plugin } from '../../../../src/core/public';
+import { SessionViewServices } from './types';
+import { getSessionViewLazy } from './methods';
+
+export class SessionViewPlugin implements Plugin {
+ public setup(core: CoreSetup) {}
+
+ public start(core: CoreStart) {
+ return {
+ getSessionView: (sessionEntityId: string) => getSessionViewLazy(sessionEntityId),
+ };
+ }
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/session_view/public/shared_imports.ts b/x-pack/plugins/session_view/public/shared_imports.ts
new file mode 100644
index 0000000000000..0a087e1ac36ae
--- /dev/null
+++ b/x-pack/plugins/session_view/public/shared_imports.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 { SectionLoading } from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/session_view/public/test/index.tsx b/x-pack/plugins/session_view/public/test/index.tsx
new file mode 100644
index 0000000000000..8570e142538de
--- /dev/null
+++ b/x-pack/plugins/session_view/public/test/index.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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, ReactNode, useMemo } from 'react';
+import { createMemoryHistory, MemoryHistory } from 'history';
+import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
+import { QueryClient, QueryClientProvider, setLogger } from 'react-query';
+import { Router } from 'react-router-dom';
+import { History } from 'history';
+import useObservable from 'react-use/lib/useObservable';
+import { I18nProvider } from '@kbn/i18n-react';
+import { CoreStart } from 'src/core/public';
+import { coreMock } from 'src/core/public/mocks';
+import { KibanaContextProvider } from 'src/plugins/kibana_react/public';
+import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
+
+type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
+
+// hide react-query output in console
+setLogger({
+ error: () => {},
+ // eslint-disable-next-line no-console
+ log: console.log,
+ // eslint-disable-next-line no-console
+ warn: console.warn,
+});
+
+/**
+ * Mocked app root context renderer
+ */
+export interface AppContextTestRender {
+ history: ReturnType;
+ coreStart: ReturnType;
+ /**
+ * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the
+ * `AppRootContext`
+ */
+ AppWrapper: React.FC;
+ /**
+ * Renders the given UI within the created `AppWrapper` providing the given UI a mocked
+ * endpoint runtime context environment
+ */
+ render: UiRender;
+}
+
+const createCoreStartMock = (
+ history: MemoryHistory
+): ReturnType => {
+ const coreStart = coreMock.createStart({ basePath: '/mock' });
+
+ // Mock the certain APP Ids returned by `application.getUrlForApp()`
+ coreStart.application.getUrlForApp.mockImplementation((appId) => {
+ switch (appId) {
+ case 'sessionView':
+ return '/app/sessionView';
+ default:
+ return `${appId} not mocked!`;
+ }
+ });
+
+ coreStart.application.navigateToUrl.mockImplementation((url) => {
+ history.push(url.replace('/app/sessionView', ''));
+ return Promise.resolve();
+ });
+
+ return coreStart;
+};
+
+const AppRootProvider = memo<{
+ history: History;
+ coreStart: CoreStart;
+ children: ReactNode | ReactNode[];
+}>(({ history, coreStart: { http, notifications, uiSettings, application }, children }) => {
+ const isDarkMode = useObservable(uiSettings.get$('theme:darkMode'));
+ const services = useMemo(
+ () => ({ http, notifications, application }),
+ [application, http, notifications]
+ );
+ return (
+
+
+
+ {children}
+
+
+
+ );
+});
+
+AppRootProvider.displayName = 'AppRootProvider';
+
+/**
+ * Creates a mocked app context custom renderer that can be used to render
+ * component that depend upon the application's surrounding context providers.
+ * Factory also returns the content that was used to create the custom renderer, allowing
+ * for further customization.
+ */
+
+export const createAppRootMockRenderer = (): AppContextTestRender => {
+ const history = createMemoryHistory();
+ const coreStart = createCoreStartMock(history);
+
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ // turns retries off
+ retry: false,
+ // prevent jest did not exit errors
+ cacheTime: Infinity,
+ },
+ },
+ });
+
+ const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
+
+ {children}
+
+ );
+
+ const render: UiRender = (ui, options = {}) => {
+ return reactRender(ui, {
+ wrapper: AppWrapper as React.ComponentType,
+ ...options,
+ });
+ };
+
+ return {
+ history,
+ coreStart,
+ AppWrapper,
+ render,
+ };
+};
diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts
new file mode 100644
index 0000000000000..2349b8423eb36
--- /dev/null
+++ b/x-pack/plugins/session_view/public/types.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ReactNode } from 'react';
+import { CoreStart } from '../../../../src/core/public';
+import { TimelinesUIStart } from '../../timelines/public';
+
+export type SessionViewServices = CoreStart & {
+ timelines: TimelinesUIStart;
+};
+
+export interface EuiTabProps {
+ id: string;
+ name: string;
+ content: ReactNode;
+ disabled?: boolean;
+ append?: ReactNode;
+ prepend?: ReactNode;
+}
+
+export interface DetailPanelProcess {
+ id: string;
+ start: string;
+ end: string;
+ exit_code: number;
+ user: string;
+ args: string[];
+ executable: string[][];
+ pid: number;
+ entryLeader: DetailPanelProcessLeader;
+ sessionLeader: DetailPanelProcessLeader;
+ groupLeader: DetailPanelProcessLeader;
+ parent: DetailPanelProcessLeader;
+}
+
+export interface DetailPanelProcessLeader {
+ id: string;
+ name: string;
+ start: string;
+ entryMetaType: string;
+ userName: string;
+ interactive: boolean;
+ pid: number;
+ entryMetaSourceIp: string;
+ executable: string;
+}
diff --git a/x-pack/plugins/session_view/public/utils/data_or_dash.test.ts b/x-pack/plugins/session_view/public/utils/data_or_dash.test.ts
new file mode 100644
index 0000000000000..12ef44cf1d708
--- /dev/null
+++ b/x-pack/plugins/session_view/public/utils/data_or_dash.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 { dataOrDash } from './data_or_dash';
+
+const TEST_STRING = '123';
+const TEST_NUMBER = 123;
+const DASH = '-';
+
+describe('dataOrDash(data)', () => {
+ it('works for a valid string', () => {
+ expect(dataOrDash(TEST_STRING)).toEqual(TEST_STRING);
+ });
+ it('works for a valid number', () => {
+ expect(dataOrDash(TEST_NUMBER)).toEqual(TEST_NUMBER);
+ });
+ it('returns dash for undefined', () => {
+ expect(dataOrDash(undefined)).toEqual(DASH);
+ });
+ it('returns dash for empty string', () => {
+ expect(dataOrDash('')).toEqual(DASH);
+ });
+ it('returns dash for NaN', () => {
+ expect(dataOrDash(NaN)).toEqual(DASH);
+ });
+});
diff --git a/x-pack/plugins/session_view/public/utils/data_or_dash.ts b/x-pack/plugins/session_view/public/utils/data_or_dash.ts
new file mode 100644
index 0000000000000..ff6c2fb9bc1ff
--- /dev/null
+++ b/x-pack/plugins/session_view/public/utils/data_or_dash.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/**
+ * Returns a dash ('-') if data is undefined, and empty string, or a NaN.
+ *
+ * Used by frontend components
+ *
+ * @param {String | Number | undefined} data
+ * @return {String | Number} either data itself or if invalid, a dash ('-')
+ */
+export const dataOrDash = (data: string | number | undefined): string | number => {
+ if (data === undefined || data === '' || (typeof data === 'number' && isNaN(data))) {
+ return '-';
+ }
+
+ return data;
+};
diff --git a/x-pack/plugins/session_view/server/index.ts b/x-pack/plugins/session_view/server/index.ts
new file mode 100644
index 0000000000000..a86684094dfd7
--- /dev/null
+++ b/x-pack/plugins/session_view/server/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { PluginInitializerContext } from '../../../../src/core/server';
+import { SessionViewPlugin } from './plugin';
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new SessionViewPlugin(initializerContext);
+}
diff --git a/x-pack/plugins/session_view/server/plugin.ts b/x-pack/plugins/session_view/server/plugin.ts
new file mode 100644
index 0000000000000..c7fd511b3de05
--- /dev/null
+++ b/x-pack/plugins/session_view/server/plugin.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+ PluginInitializerContext,
+} from '../../../../src/core/server';
+import { SessionViewSetupPlugins, SessionViewStartPlugins } from './types';
+import { registerRoutes } from './routes';
+
+export class SessionViewPlugin implements Plugin {
+ private logger: Logger;
+
+ /**
+ * Initialize SessionViewPlugin class properties (logger, etc) that is accessible
+ * through the initializerContext.
+ */
+ constructor(initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ }
+
+ public setup(core: CoreSetup, plugins: SessionViewSetupPlugins) {
+ this.logger.debug('session view: Setup');
+ const router = core.http.createRouter();
+
+ // Register server routes
+ registerRoutes(router);
+ }
+
+ public start(core: CoreStart, plugins: SessionViewStartPlugins) {
+ this.logger.debug('session view: Start');
+ }
+
+ public stop() {
+ this.logger.debug('session view: Stop');
+ }
+}
diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts
new file mode 100644
index 0000000000000..7b9cfb45f580b
--- /dev/null
+++ b/x-pack/plugins/session_view/server/routes/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 { registerProcessEventsRoute } from './process_events_route';
+import { sessionEntryLeadersRoute } from './session_entry_leaders_route';
+
+export const registerRoutes = (router: IRouter) => {
+ registerProcessEventsRoute(router);
+ sessionEntryLeadersRoute(router);
+};
diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.test.ts b/x-pack/plugins/session_view/server/routes/process_events_route.test.ts
new file mode 100644
index 0000000000000..76f54eb4b8ab6
--- /dev/null
+++ b/x-pack/plugins/session_view/server/routes/process_events_route.test.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 { elasticsearchServiceMock } from 'src/core/server/mocks';
+import { doSearch } from './process_events_route';
+import { mockEvents } from '../../common/mocks/constants/session_view_process.mock';
+
+const getEmptyResponse = async () => {
+ return {
+ hits: {
+ total: { value: 0, relation: 'eq' },
+ hits: [],
+ },
+ };
+};
+
+const getResponse = async () => {
+ return {
+ hits: {
+ total: { value: mockEvents.length, relation: 'eq' },
+ hits: mockEvents.map((event) => {
+ return { _source: event };
+ }),
+ },
+ };
+};
+
+describe('process_events_route.ts', () => {
+ describe('doSearch(client, entityId, cursor, forward)', () => {
+ it('should return an empty events array for a non existant entity_id', async () => {
+ const client = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse());
+
+ const body = await doSearch(client, 'asdf', undefined);
+
+ expect(body.events.length).toBe(0);
+ });
+
+ it('returns results for a particular session entity_id', async () => {
+ const client = elasticsearchServiceMock.createElasticsearchClient(getResponse());
+
+ const body = await doSearch(client, 'mockId', undefined);
+
+ expect(body.events.length).toBe(mockEvents.length);
+ });
+
+ it('returns hits in reverse order when paginating backwards', async () => {
+ const client = elasticsearchServiceMock.createElasticsearchClient(getResponse());
+
+ const body = await doSearch(client, 'mockId', undefined, false);
+
+ expect(body.events[0]._source).toEqual(mockEvents[mockEvents.length - 1]);
+ });
+ });
+});
diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts
new file mode 100644
index 0000000000000..47e2d917733d5
--- /dev/null
+++ b/x-pack/plugins/session_view/server/routes/process_events_route.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 { schema } from '@kbn/config-schema';
+import type { ElasticsearchClient } from 'kibana/server';
+import { IRouter } from '../../../../../src/core/server';
+import {
+ PROCESS_EVENTS_ROUTE,
+ PROCESS_EVENTS_PER_PAGE,
+ PROCESS_EVENTS_INDEX,
+ ALERTS_INDEX,
+ ENTRY_SESSION_ENTITY_ID_PROPERTY,
+} from '../../common/constants';
+import { expandDottedObject } from '../../common/utils/expand_dotted_object';
+
+export const registerProcessEventsRoute = (router: IRouter) => {
+ router.get(
+ {
+ path: PROCESS_EVENTS_ROUTE,
+ validate: {
+ query: schema.object({
+ sessionEntityId: schema.string(),
+ cursor: schema.maybe(schema.string()),
+ forward: schema.maybe(schema.boolean()),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ const client = context.core.elasticsearch.client.asCurrentUser;
+ const { sessionEntityId, cursor, forward = true } = request.query;
+ const body = await doSearch(client, sessionEntityId, cursor, forward);
+
+ return response.ok({ body });
+ }
+ );
+};
+
+export const doSearch = async (
+ client: ElasticsearchClient,
+ sessionEntityId: string,
+ cursor: string | undefined,
+ forward = true
+) => {
+ const search = await client.search({
+ // TODO: move alerts into it's own route with it's own pagination.
+ index: [PROCESS_EVENTS_INDEX, ALERTS_INDEX],
+ ignore_unavailable: true,
+ body: {
+ query: {
+ match: {
+ [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId,
+ },
+ },
+ // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available
+ // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS
+ runtime_mappings: {
+ [ENTRY_SESSION_ENTITY_ID_PROPERTY]: {
+ type: 'keyword',
+ },
+ },
+ size: PROCESS_EVENTS_PER_PAGE,
+ sort: [{ '@timestamp': forward ? 'asc' : 'desc' }],
+ search_after: cursor ? [cursor] : undefined,
+ },
+ });
+
+ const events = search.hits.hits.map((hit: any) => {
+ // TODO: re-eval if this is needed after moving alerts to it's own route.
+ // the .siem-signals-default index flattens many properties. this util unflattens them.
+ hit._source = expandDottedObject(hit._source);
+
+ return hit;
+ });
+
+ if (!forward) {
+ events.reverse();
+ }
+
+ return {
+ events,
+ };
+};
diff --git a/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts b/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts
new file mode 100644
index 0000000000000..98aee357fb91e
--- /dev/null
+++ b/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.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 { schema } from '@kbn/config-schema';
+import { IRouter } from '../../../../../src/core/server';
+import { SESSION_ENTRY_LEADERS_ROUTE, PROCESS_EVENTS_INDEX } from '../../common/constants';
+
+export const sessionEntryLeadersRoute = (router: IRouter) => {
+ router.get(
+ {
+ path: SESSION_ENTRY_LEADERS_ROUTE,
+ validate: {
+ query: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ const client = context.core.elasticsearch.client.asCurrentUser;
+ const { id } = request.query;
+
+ const result = await client.get({
+ index: PROCESS_EVENTS_INDEX,
+ id,
+ });
+
+ return response.ok({
+ body: {
+ session_entry_leader: result?._source,
+ },
+ });
+ }
+ );
+};
diff --git a/x-pack/plugins/session_view/server/types.ts b/x-pack/plugins/session_view/server/types.ts
new file mode 100644
index 0000000000000..0d1375081ca87
--- /dev/null
+++ b/x-pack/plugins/session_view/server/types.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SessionViewSetupPlugins {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SessionViewStartPlugins {}
diff --git a/x-pack/plugins/session_view/tsconfig.json b/x-pack/plugins/session_view/tsconfig.json
new file mode 100644
index 0000000000000..a99e83976a31d
--- /dev/null
+++ b/x-pack/plugins/session_view/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ // add all the folders containg files to be compiled
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ "server/**/*.json",
+ "scripts/**/*",
+ "package.json",
+ "storybook/**/*",
+ "../../../typings/**/*"
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ // add references to other TypeScript projects the plugin depends on
+
+ // requiredPlugins from ./kibana.json
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json" },
+ { "path": "../encrypted_saved_objects/tsconfig.json" },
+
+ // optionalPlugins from ./kibana.json
+ { "path": "../security/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ { "path": "../cloud/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+
+ // requiredBundles from ./kibana.json
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
+ { "path": "../infra/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }
+ ]
+}
diff --git a/yarn.lock b/yarn.lock
index 753a9e5d9805c..6867f4edc8191 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24737,10 +24737,10 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"
-react-query@^3.34.0:
- version "3.34.8"
- resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.8.tgz#a3be8523fd95f766b04c32847a1b58d8231db03c"
- integrity sha512-pl9e2VmVbgKf29Qn/WpmFVtB2g17JPqLLyOQg3GfSs/S2WABvip5xlT464vfXtilLPcJVg9bEHHlqmC38/nvDw==
+react-query@^3.34.7:
+ version "3.34.7"
+ resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.7.tgz#e3d71318f510ea354794cd188b351bb57f577cb9"
+ integrity sha512-Q8+H2DgpoZdGUpwW2Z9WAbSrIE+yOdZiCUokHjlniOOmlcsfqNLgvHF5i7rtuCmlw3hv5OAhtpS7e97/DvgpWw==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
From b51ae8bbddb8f8cc2bc83ca87918036c690b7659 Mon Sep 17 00:00:00 2001
From: Kyle Pollich
Date: Fri, 4 Mar 2022 12:01:59 -0500
Subject: [PATCH 05/27] Empty fleet_package.json on main (#126920)
---
fleet_packages.json | 23 +----------------------
1 file changed, 1 insertion(+), 22 deletions(-)
diff --git a/fleet_packages.json b/fleet_packages.json
index 69fd83f12037c..3657057ad3431 100644
--- a/fleet_packages.json
+++ b/fleet_packages.json
@@ -12,25 +12,4 @@
in order to verify package integrity.
*/
-[
- {
- "name": "apm",
- "version": "8.1.0"
- },
- {
- "name": "elastic_agent",
- "version": "1.3.0"
- },
- {
- "name": "endpoint",
- "version": "1.5.0"
- },
- {
- "name": "fleet_server",
- "version": "1.1.0"
- },
- {
- "name": "synthetics",
- "version": "0.9.2"
- }
-]
+[]
From f7f8d6da9a7188a767f92c19931da5c2f950305b Mon Sep 17 00:00:00 2001
From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
Date: Fri, 4 Mar 2022 09:04:36 -0800
Subject: [PATCH 06/27] [ResponseOps] Mapped/searchable params (#126531)
* Mapped params implementation with unit/integration/migration tests
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/alerting/common/alert.ts | 8 +
.../lib/mapped_params_utils.test.ts | 131 +++++++++++++
.../rules_client/lib/mapped_params_utils.ts | 172 ++++++++++++++++++
.../lib/validate_attributes.test.ts | 26 ++-
.../rules_client/lib/validate_attributes.ts | 77 ++++++--
.../server/rules_client/rules_client.ts | 46 ++++-
.../server/rules_client/tests/create.test.ts | 161 ++++++++++++++++
.../server/rules_client/tests/find.test.ts | 31 ++++
.../server/rules_client/tests/update.test.ts | 8 +
.../server/saved_objects/mappings.json | 10 +
.../server/saved_objects/migrations.test.ts | 24 +++
.../server/saved_objects/migrations.ts | 30 +++
x-pack/plugins/alerting/server/types.ts | 2 +
.../rules/all/use_columns.tsx | 4 +-
.../spaces_only/tests/alerting/create.ts | 48 +++++
.../spaces_only/tests/alerting/find.ts | 74 ++++++++
.../spaces_only/tests/alerting/migrations.ts | 16 ++
.../spaces_only/tests/alerting/update.ts | 16 +-
.../functional/es_archives/alerts/data.json | 42 +++++
19 files changed, 907 insertions(+), 19 deletions(-)
create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts
create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts
diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts
index 35058aa343b1a..da916ee7ed98a 100644
--- a/x-pack/plugins/alerting/common/alert.ts
+++ b/x-pack/plugins/alerting/common/alert.ts
@@ -63,6 +63,13 @@ export interface AlertAggregations {
ruleMutedStatus: { muted: number; unmuted: number };
}
+export interface MappedParamsProperties {
+ risk_score?: number;
+ severity?: string;
+}
+
+export type MappedParams = SavedObjectAttributes & MappedParamsProperties;
+
export interface Alert {
id: string;
enabled: boolean;
@@ -73,6 +80,7 @@ export interface Alert {
schedule: IntervalSchedule;
actions: AlertAction[];
params: Params;
+ mapped_params?: MappedParams;
scheduledTaskId?: string;
createdBy: string | null;
updatedBy: string | null;
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts
new file mode 100644
index 0000000000000..d8618d0ed6c21
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { fromKueryExpression } from '@kbn/es-query';
+import {
+ getMappedParams,
+ getModifiedFilter,
+ getModifiedField,
+ getModifiedSearchFields,
+ getModifiedSearch,
+ getModifiedValue,
+ modifyFilterKueryNode,
+} from './mapped_params_utils';
+
+describe('getModifiedParams', () => {
+ it('converts params to mapped params', () => {
+ const params = {
+ riskScore: 42,
+ severity: 'medium',
+ a: 'test',
+ b: 'test',
+ c: 'test,',
+ };
+
+ expect(getMappedParams(params)).toEqual({
+ risk_score: 42,
+ severity: '40-medium',
+ });
+ });
+
+ it('returns empty mapped params if nothing exists in the input params', () => {
+ const params = {
+ a: 'test',
+ b: 'test',
+ c: 'test',
+ };
+
+ expect(getMappedParams(params)).toEqual({});
+ });
+});
+
+describe('getModifiedFilter', () => {
+ it('converts params filters to mapped params filters', () => {
+ // Make sure it works for both camel and snake case params
+ const filter = 'alert.attributes.params.risk_score: 45';
+
+ expect(getModifiedFilter(filter)).toEqual('alert.attributes.mapped_params.risk_score: 45');
+ });
+});
+
+describe('getModifiedField', () => {
+ it('converts sort field to mapped params sort field', () => {
+ expect(getModifiedField('params.risk_score')).toEqual('mapped_params.risk_score');
+ expect(getModifiedField('params.riskScore')).toEqual('mapped_params.risk_score');
+ expect(getModifiedField('params.invalid')).toEqual('params.invalid');
+ });
+});
+
+describe('getModifiedSearchFields', () => {
+ it('converts a list of params search fields to mapped param search fields', () => {
+ const searchFields = [
+ 'params.risk_score',
+ 'params.riskScore',
+ 'params.severity',
+ 'params.invalid',
+ 'invalid',
+ ];
+
+ expect(getModifiedSearchFields(searchFields)).toEqual([
+ 'mapped_params.risk_score',
+ 'mapped_params.risk_score',
+ 'mapped_params.severity',
+ 'params.invalid',
+ 'invalid',
+ ]);
+ });
+});
+
+describe('getModifiedSearch', () => {
+ it('converts the search value depending on the search field', () => {
+ const searchFields = ['params.severity', 'another'];
+
+ expect(getModifiedSearch(searchFields, 'medium')).toEqual('40-medium');
+ expect(getModifiedSearch(searchFields, 'something else')).toEqual('something else');
+ expect(getModifiedSearch('params.risk_score', 'something else')).toEqual('something else');
+ expect(getModifiedSearch('mapped_params.severity', 'medium')).toEqual('40-medium');
+ });
+});
+
+describe('getModifiedValue', () => {
+ it('converts severity strings to sortable strings', () => {
+ expect(getModifiedValue('severity', 'low')).toEqual('20-low');
+ expect(getModifiedValue('severity', 'medium')).toEqual('40-medium');
+ expect(getModifiedValue('severity', 'high')).toEqual('60-high');
+ expect(getModifiedValue('severity', 'critical')).toEqual('80-critical');
+ });
+});
+
+describe('modifyFilterKueryNode', () => {
+ it('modifies the resulting kuery node AST filter for alert params', () => {
+ const astFilter = fromKueryExpression(
+ 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.params.severity > medium'
+ );
+
+ expect(astFilter.arguments[2].arguments[0]).toEqual({
+ type: 'literal',
+ value: 'alert.attributes.params.severity',
+ });
+
+ expect(astFilter.arguments[2].arguments[2]).toEqual({
+ type: 'literal',
+ value: 'medium',
+ });
+
+ modifyFilterKueryNode({ astFilter });
+
+ expect(astFilter.arguments[2].arguments[0]).toEqual({
+ type: 'literal',
+ value: 'alert.attributes.mapped_params.severity',
+ });
+
+ expect(astFilter.arguments[2].arguments[2]).toEqual({
+ type: 'literal',
+ value: '40-medium',
+ });
+ });
+});
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts
new file mode 100644
index 0000000000000..b4d82990654c2
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts
@@ -0,0 +1,172 @@
+/*
+ * 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 { snakeCase } from 'lodash';
+import { AlertTypeParams, MappedParams, MappedParamsProperties } from '../../types';
+import { SavedObjectAttribute } from '../../../../../../src/core/server';
+import {
+ iterateFilterKureyNode,
+ IterateFilterKureyNodeParams,
+ IterateActionProps,
+ getFieldNameAttribute,
+} from './validate_attributes';
+
+export const MAPPED_PARAMS_PROPERTIES: Array = [
+ 'risk_score',
+ 'severity',
+];
+
+const SEVERITY_MAP: Record = {
+ low: '20-low',
+ medium: '40-medium',
+ high: '60-high',
+ critical: '80-critical',
+};
+
+/**
+ * Returns the mapped_params object when given a params object.
+ * The function will match params present in MAPPED_PARAMS_PROPERTIES and
+ * return an empty object if nothing is matched.
+ */
+export const getMappedParams = (params: AlertTypeParams) => {
+ return Object.entries(params).reduce((result, [key, value]) => {
+ const snakeCaseKey = snakeCase(key);
+
+ if (MAPPED_PARAMS_PROPERTIES.includes(snakeCaseKey as keyof MappedParamsProperties)) {
+ result[snakeCaseKey] = getModifiedValue(
+ snakeCaseKey,
+ value as string
+ ) as SavedObjectAttribute;
+ }
+
+ return result;
+ }, {});
+};
+
+/**
+ * Returns a string of the filter, but with params replaced with mapped_params.
+ * This function will check both camel and snake case to make sure we're consistent
+ * with the naming
+ *
+ * i.e.: 'alerts.attributes.params.riskScore' -> 'alerts.attributes.mapped_params.risk_score'
+ */
+export const getModifiedFilter = (filter: string) => {
+ return filter.replace('.params.', '.mapped_params.');
+};
+
+/**
+ * Returns modified field with mapped_params instead of params.
+ *
+ * i.e.: 'params.riskScore' -> 'mapped_params.risk_score'
+ */
+export const getModifiedField = (field: string | undefined) => {
+ if (!field) {
+ return field;
+ }
+
+ const sortFieldToReplace = `${snakeCase(field.replace('params.', ''))}`;
+
+ if (MAPPED_PARAMS_PROPERTIES.includes(sortFieldToReplace as keyof MappedParamsProperties)) {
+ return `mapped_params.${sortFieldToReplace}`;
+ }
+
+ return field;
+};
+
+/**
+ * Returns modified search fields with mapped_params instead of params.
+ *
+ * i.e.:
+ * [
+ * 'params.riskScore',
+ * 'params.severity',
+ * ]
+ * ->
+ * [
+ * 'mapped_params.riskScore',
+ * 'mapped_params.severity',
+ * ]
+ */
+export const getModifiedSearchFields = (searchFields: string[] | undefined) => {
+ if (!searchFields) {
+ return searchFields;
+ }
+
+ return searchFields.reduce((result, field) => {
+ const modifiedField = getModifiedField(field);
+ if (modifiedField) {
+ return [...result, modifiedField];
+ }
+ return result;
+ }, []);
+};
+
+export const getModifiedValue = (key: string, value: string) => {
+ if (key === 'severity') {
+ return SEVERITY_MAP[value] || '';
+ }
+ return value;
+};
+
+export const getModifiedSearch = (searchFields: string | string[] | undefined, value: string) => {
+ if (!searchFields) {
+ return value;
+ }
+
+ const fieldNames = Array.isArray(searchFields) ? searchFields : [searchFields];
+
+ const modifiedSearchValues = fieldNames.map((fieldName) => {
+ const firstAttribute = getFieldNameAttribute(fieldName, [
+ 'alert',
+ 'attributes',
+ 'params',
+ 'mapped_params',
+ ]);
+ return getModifiedValue(firstAttribute, value);
+ });
+
+ return modifiedSearchValues.find((search) => search !== value) || value;
+};
+
+export const modifyFilterKueryNode = ({
+ astFilter,
+ hasNestedKey = false,
+ nestedKeys,
+ storeValue,
+ path = 'arguments',
+}: IterateFilterKureyNodeParams) => {
+ const action = ({ index, ast, fieldName, localFieldName }: IterateActionProps) => {
+ // First index, assuming ast value is the attribute name
+ if (index === 0) {
+ const firstAttribute = getFieldNameAttribute(fieldName, ['alert', 'attributes']);
+ // Replace the ast.value for params to mapped_params
+ if (firstAttribute === 'params') {
+ ast.value = getModifiedFilter(ast.value);
+ }
+ }
+
+ // Subsequent indices, assuming ast value is the filtering value
+ else {
+ const firstAttribute = getFieldNameAttribute(localFieldName, ['alert', 'attributes']);
+
+ // Replace the ast.value for params value to the modified mapped_params value
+ if (firstAttribute === 'params' && ast.value) {
+ const attribute = getFieldNameAttribute(localFieldName, ['alert', 'attributes', 'params']);
+ ast.value = getModifiedValue(attribute, ast.value);
+ }
+ }
+ };
+
+ iterateFilterKureyNode({
+ astFilter,
+ hasNestedKey,
+ nestedKeys,
+ storeValue,
+ path,
+ action,
+ });
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts
index 652c30ff380c5..1777a36d80a2f 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts
@@ -13,7 +13,7 @@ import {
} from './validate_attributes';
describe('Validate attributes', () => {
- const excludedFieldNames = ['monitoring'];
+ const excludedFieldNames = ['monitoring', 'mapped_params'];
describe('validateSortField', () => {
test('should NOT throw an error, when sort field is not part of the field to exclude', () => {
expect(() => validateSortField('name.keyword', excludedFieldNames)).not.toThrow();
@@ -86,6 +86,17 @@ describe('Validate attributes', () => {
).not.toThrow();
});
+ test('should NOT throw an error, when filter contains params with validate properties', () => {
+ expect(() =>
+ validateFilterKueryNode({
+ astFilter: esKuery.fromKueryExpression(
+ 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.params.risk_score > 50'
+ ),
+ excludedFieldNames,
+ })
+ ).not.toThrow();
+ });
+
test('should throw an error, when filter contains the field to exclude', () => {
expect(() =>
validateFilterKueryNode({
@@ -111,5 +122,18 @@ describe('Validate attributes', () => {
`"Filter is not supported on this field alert.attributes.actions"`
);
});
+
+ test('should throw an error, when filtering contains a property that is not valid', () => {
+ expect(() =>
+ validateFilterKueryNode({
+ astFilter: esKuery.fromKueryExpression(
+ 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.mapped_params.risk_score > 50'
+ ),
+ excludedFieldNames,
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Filter is not supported on this field alert.attributes.mapped_params.risk_score"`
+ );
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts
index fa65f4c2f0999..ad17ede1b99ad 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts
+++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts
@@ -7,11 +7,18 @@
import { KueryNode } from '@kbn/es-query';
import { get, isEmpty } from 'lodash';
-
import mappings from '../../saved_objects/mappings.json';
const astFunctionType = ['is', 'range', 'nested'];
+export const getFieldNameAttribute = (fieldName: string, attributesToIgnore: string[]) => {
+ const fieldNameSplit = (fieldName || '')
+ .split('.')
+ .filter((fn: string) => !attributesToIgnore.includes(fn));
+
+ return fieldNameSplit.length > 0 ? fieldNameSplit[0] : '';
+};
+
export const validateOperationOnAttributes = (
astFilter: KueryNode | null,
sortField: string | undefined,
@@ -44,28 +51,41 @@ export const validateSearchFields = (searchFields: string[], excludedFieldNames:
}
};
-interface ValidateFilterKueryNodeParams {
+export interface IterateActionProps {
+ ast: KueryNode;
+ index: number;
+ fieldName: string;
+ localFieldName: string;
+}
+
+export interface IterateFilterKureyNodeParams {
astFilter: KueryNode;
- excludedFieldNames: string[];
hasNestedKey?: boolean;
nestedKeys?: string;
storeValue?: boolean;
path?: string;
+ action?: (props: IterateActionProps) => void;
}
-export const validateFilterKueryNode = ({
+export interface ValidateFilterKueryNodeParams extends IterateFilterKureyNodeParams {
+ excludedFieldNames: string[];
+}
+
+export const iterateFilterKureyNode = ({
astFilter,
- excludedFieldNames,
hasNestedKey = false,
nestedKeys,
storeValue,
path = 'arguments',
-}: ValidateFilterKueryNodeParams) => {
+ action = () => {},
+}: IterateFilterKureyNodeParams) => {
let localStoreValue = storeValue;
let localNestedKeys: string | undefined;
+ let localFieldName: string = '';
if (localStoreValue === undefined) {
localStoreValue = astFilter.type === 'function' && astFunctionType.includes(astFilter.function);
}
+
astFilter.arguments.forEach((ast: KueryNode, index: number) => {
if (hasNestedKey && ast.type === 'literal' && ast.value != null) {
localNestedKeys = ast.value;
@@ -80,25 +100,56 @@ export const validateFilterKueryNode = ({
if (ast.arguments) {
const myPath = `${path}.${index}`;
- validateFilterKueryNode({
+ iterateFilterKureyNode({
astFilter: ast,
- excludedFieldNames,
storeValue: ast.type === 'function' && astFunctionType.includes(ast.function),
path: `${myPath}.arguments`,
hasNestedKey: ast.type === 'function' && ast.function === 'nested',
nestedKeys: localNestedKeys || nestedKeys,
+ action,
});
}
- if (localStoreValue && index === 0) {
+ if (localStoreValue) {
const fieldName = nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value;
- const fieldNameSplit = fieldName
- .split('.')
- .filter((fn: string) => !['alert', 'attributes'].includes(fn));
- const firstAttribute = fieldNameSplit.length > 0 ? fieldNameSplit[0] : '';
+
+ if (index === 0) {
+ localFieldName = fieldName;
+ }
+
+ action({
+ ast,
+ index,
+ fieldName,
+ localFieldName,
+ });
+ }
+ });
+};
+
+export const validateFilterKueryNode = ({
+ astFilter,
+ excludedFieldNames,
+ hasNestedKey = false,
+ nestedKeys,
+ storeValue,
+ path = 'arguments',
+}: ValidateFilterKueryNodeParams) => {
+ const action = ({ index, fieldName }: IterateActionProps) => {
+ if (index === 0) {
+ const firstAttribute = getFieldNameAttribute(fieldName, ['alert', 'attributes']);
if (excludedFieldNames.includes(firstAttribute)) {
throw new Error(`Filter is not supported on this field ${fieldName}`);
}
}
+ };
+
+ iterateFilterKureyNode({
+ astFilter,
+ hasNestedKey,
+ nestedKeys,
+ storeValue,
+ path,
+ action,
});
};
diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
index 6d3ffc822a626..86f0d3becdce7 100644
--- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts
+++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
@@ -78,6 +78,13 @@ import { Alert } from '../alert';
import { EVENT_LOG_ACTIONS } from '../plugin';
import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
import { getDefaultRuleMonitoring } from '../task_runner/task_runner';
+import {
+ getMappedParams,
+ getModifiedField,
+ getModifiedSearchFields,
+ getModifiedSearch,
+ modifyFilterKueryNode,
+} from './lib/mapped_params_utils';
export interface RegistryAlertTypeWithAuth extends RegistryRuleType {
authorizedConsumers: string[];
@@ -251,7 +258,10 @@ export class RulesClient {
private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
private readonly auditLogger?: AuditLogger;
private readonly eventLogger?: IEventLogger;
- private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring'];
+ private readonly fieldsToExcludeFromPublicApi: Array = [
+ 'monitoring',
+ 'mapped_params',
+ ];
constructor({
ruleTypeRegistry,
@@ -371,6 +381,12 @@ export class RulesClient {
monitoring: getDefaultRuleMonitoring(),
};
+ const mappedParams = getMappedParams(updatedParams);
+
+ if (Object.keys(mappedParams).length) {
+ rawRule.mapped_params = mappedParams;
+ }
+
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.CREATE,
@@ -634,9 +650,10 @@ export class RulesClient {
);
throw error;
}
+
const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple;
const filterKueryNode = options.filter ? esKuery.fromKueryExpression(options.filter) : null;
- const sortField = mapSortField(options.sortField);
+ let sortField = mapSortField(options.sortField);
if (excludeFromPublicApi) {
try {
validateOperationOnAttributes(
@@ -650,6 +667,24 @@ export class RulesClient {
}
}
+ sortField = mapSortField(getModifiedField(options.sortField));
+
+ // Generate new modified search and search fields, translating certain params properties
+ // to mapped_params. Thus, allowing for sort/search/filtering on params.
+ // We do the modifcation after the validate check to make sure the public API does not
+ // use the mapped_params in their queries.
+ options = {
+ ...options,
+ ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }),
+ ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }),
+ };
+
+ // Modifies kuery node AST to translate params filter and the filter value to mapped_params.
+ // This translation is done in place, and therefore is not a pure function.
+ if (filterKueryNode) {
+ modifyFilterKueryNode({ astFilter: filterKueryNode });
+ }
+
const {
page,
per_page: perPage,
@@ -1027,6 +1062,13 @@ export class RulesClient {
updatedBy: username,
updatedAt: new Date().toISOString(),
});
+
+ const mappedParams = getMappedParams(updatedParams);
+
+ if (Object.keys(mappedParams).length) {
+ createAttributes.mapped_params = mappedParams;
+ }
+
try {
updatedObject = await this.unsecuredSavedObjectsClient.create(
'alert',
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
index 6ccc640dcc135..8cecb47f23a88 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
@@ -1878,6 +1878,167 @@ describe('create()', () => {
`);
});
+ test('should create alerts with mapped_params', async () => {
+ const data = getMockData({
+ params: {
+ bar: true,
+ risk_score: 42,
+ severity: 'low',
+ },
+ });
+
+ const createdAttributes = {
+ ...data,
+ alertTypeId: '123',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ risk_score: 42,
+ severity: 'low',
+ },
+ createdAt: '2019-02-12T21:01:22.479Z',
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
+ muteAll: false,
+ mutedInstanceIds: [],
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: 'test',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ };
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '123',
+ type: 'alert',
+ attributes: createdAttributes,
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ });
+
+ const result = await rulesClient.create({ data });
+
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
+ 'alert',
+ {
+ enabled: true,
+ name: 'abc',
+ tags: ['foo'],
+ alertTypeId: '123',
+ consumer: 'bar',
+ schedule: {
+ interval: '1m',
+ },
+ throttle: null,
+ notifyWhen: 'onActiveAlert',
+ params: {
+ bar: true,
+ risk_score: 42,
+ severity: 'low',
+ },
+ actions: [
+ {
+ group: 'default',
+ params: {
+ foo: true,
+ },
+ actionRef: 'action_0',
+ actionTypeId: 'test',
+ },
+ ],
+ apiKeyOwner: null,
+ apiKey: null,
+ legacyId: null,
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+ createdAt: '2019-02-12T21:01:22.479Z',
+ updatedAt: '2019-02-12T21:01:22.479Z',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'pending',
+ lastExecutionDate: '2019-02-12T21:01:22.479Z',
+ error: null,
+ },
+ monitoring: {
+ execution: {
+ history: [],
+ calculated_metrics: {
+ success_ratio: 0,
+ },
+ },
+ },
+ mapped_params: {
+ risk_score: 42,
+ severity: '20-low',
+ },
+ meta: {
+ versionApiKeyLastmodified: 'v8.0.0',
+ },
+ },
+ {
+ references: [
+ {
+ id: '1',
+ name: 'action_0',
+ type: 'action',
+ },
+ ],
+ id: 'mock-saved-object-id',
+ }
+ );
+
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "actions": Array [
+ Object {
+ "actionTypeId": "test",
+ "group": "default",
+ "id": "1",
+ "params": Object {
+ "foo": true,
+ },
+ },
+ ],
+ "alertTypeId": "123",
+ "consumer": "bar",
+ "createdAt": 2019-02-12T21:01:22.479Z,
+ "createdBy": "elastic",
+ "enabled": true,
+ "id": "123",
+ "muteAll": false,
+ "mutedInstanceIds": Array [],
+ "name": "abc",
+ "notifyWhen": null,
+ "params": Object {
+ "bar": true,
+ "risk_score": 42,
+ "severity": "low",
+ },
+ "schedule": Object {
+ "interval": "10s",
+ },
+ "scheduledTaskId": "task-123",
+ "tags": Array [
+ "foo",
+ ],
+ "throttle": null,
+ "updatedAt": 2019-02-12T21:01:22.479Z,
+ "updatedBy": "elastic",
+ }
+ `);
+ });
+
test('should validate params', async () => {
const data = getMockData();
ruleTypeRegistry.get.mockReturnValue({
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts
index 60aac3f266e78..bd382faa6d6cb 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts
@@ -290,6 +290,37 @@ describe('find()', () => {
expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name');
});
+ test('should translate filter/sort/search on params to mapped_params', async () => {
+ const filter = esKuery.fromKueryExpression(
+ '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))'
+ );
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ filter,
+ ensureRuleTypeIsAuthorized() {},
+ });
+
+ const rulesClient = new RulesClient(rulesClientParams);
+ await rulesClient.find({
+ options: {
+ sortField: 'params.risk_score',
+ searchFields: ['params.risk_score', 'params.severity'],
+ filter: 'alert.attributes.params.risk_score > 50',
+ },
+ excludeFromPublicApi: true,
+ });
+
+ const findCallParams = unsecuredSavedObjectsClient.find.mock.calls[0][0];
+
+ expect(findCallParams.searchFields).toEqual([
+ 'mapped_params.risk_score',
+ 'mapped_params.severity',
+ ]);
+
+ expect(findCallParams.filter.arguments[0].arguments[0].value).toEqual(
+ 'alert.attributes.mapped_params.risk_score'
+ );
+ });
+
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
jest.resetAllMocks();
authorization.getFindAuthorizationFilter.mockResolvedValue({
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
index 1def4b7d60f4e..be2f859ac96b3 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
@@ -252,6 +252,8 @@ describe('update()', () => {
tags: ['foo'],
params: {
bar: true,
+ risk_score: 40,
+ severity: 'low',
},
throttle: null,
notifyWhen: 'onActiveAlert',
@@ -362,6 +364,10 @@ describe('update()', () => {
"apiKeyOwner": null,
"consumer": "myApp",
"enabled": true,
+ "mapped_params": Object {
+ "risk_score": 40,
+ "severity": "20-low",
+ },
"meta": Object {
"versionApiKeyLastmodified": "v7.10.0",
},
@@ -369,6 +375,8 @@ describe('update()', () => {
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
+ "risk_score": 40,
+ "severity": "low",
},
"schedule": Object {
"interval": "1m",
diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json
index 806e72fa33d5d..e6eedced78914 100644
--- a/x-pack/plugins/alerting/server/saved_objects/mappings.json
+++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json
@@ -53,6 +53,16 @@
"type": "flattened",
"ignore_above": 4096
},
+ "mapped_params": {
+ "properties": {
+ "risk_score": {
+ "type": "float"
+ },
+ "severity": {
+ "type": "keyword"
+ }
+ }
+ },
"scheduledTaskId": {
"type": "keyword"
},
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 1d7d3d2a362a9..28b1f599f9575 100644
--- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts
+++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts
@@ -2229,6 +2229,30 @@ describe('successful migrations', () => {
);
});
+ describe('8.2.0', () => {
+ test('migrates params to mapped_params', () => {
+ const migration820 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.2.0'];
+ const alert = getMockData(
+ {
+ params: {
+ risk_score: 60,
+ severity: 'high',
+ foo: 'bar',
+ },
+ alertTypeId: 'siem.signals',
+ },
+ true
+ );
+
+ const migratedAlert820 = migration820(alert, migrationContext);
+
+ expect(migratedAlert820.attributes.mapped_params).toEqual({
+ risk_score: 60,
+ severity: '60-high',
+ });
+ });
+ });
+
describe('Metrics Inventory Threshold rule', () => {
test('Migrates incorrect action group spelling', () => {
const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0'];
diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts
index 6e6c886d91b53..09d505aec0f0c 100644
--- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts
+++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts
@@ -21,6 +21,7 @@ import { RawRule, RawAlertAction } from '../types';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server';
import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations';
+import { getMappedParams } from '../../server/rules_client/lib/mapped_params_utils';
const SIEM_APP_ID = 'securitySolution';
const SIEM_SERVER_APP_ID = 'siem';
@@ -145,6 +146,12 @@ export function getMigrations(
pipeMigrations(addSecuritySolutionAADRuleTypeTags)
);
+ const migrationRules820 = createEsoMigration(
+ encryptedSavedObjects,
+ (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true,
+ pipeMigrations(addMappedParams)
+ );
+
return {
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'),
@@ -155,6 +162,7 @@ export function getMigrations(
'7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'),
'8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'),
'8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'),
+ '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'),
};
}
@@ -822,6 +830,28 @@ function fixInventoryThresholdGroupId(
}
}
+function addMappedParams(
+ doc: SavedObjectUnsanitizedDoc
+): SavedObjectUnsanitizedDoc {
+ const {
+ attributes: { params },
+ } = doc;
+
+ const mappedParams = getMappedParams(params);
+
+ if (Object.keys(mappedParams).length) {
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ mapped_params: mappedParams,
+ },
+ };
+ }
+
+ return doc;
+}
+
function getCorrespondingAction(
actions: SavedObjectAttribute,
connectorRef: string
diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts
index 8a0b61fed787a..6b06f7efe3066 100644
--- a/x-pack/plugins/alerting/server/types.ts
+++ b/x-pack/plugins/alerting/server/types.ts
@@ -40,6 +40,7 @@ import {
ActionVariable,
SanitizedRuleConfig,
RuleMonitoring,
+ MappedParams,
} from '../common';
import { LicenseType } from '../../licensing/server';
import { IAbortableClusterClient } from './lib/create_abortable_es_client_factory';
@@ -236,6 +237,7 @@ export interface RawRule extends SavedObjectAttributes {
schedule: SavedObjectAttributes;
actions: RawAlertAction[];
params: SavedObjectAttributes;
+ mapped_params?: MappedParams;
scheduledTaskId?: string | null;
createdBy: string | null;
updatedBy: string | null;
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
index f241a3df87327..3788203008238 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
@@ -196,7 +196,7 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
{value}
),
- sortable: !!isInMemorySorting,
+ sortable: true,
truncateText: true,
width: '85px',
},
@@ -204,7 +204,7 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
field: 'severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => ,
- sortable: !!isInMemorySorting,
+ sortable: true,
truncateText: true,
width: '12%',
},
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
index 7eb7cf5efc7d3..b002e0668dc52 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
@@ -272,6 +272,54 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
});
});
+ it('should create rules with mapped parameters', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ connector_type_id: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
+ const createResponse = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ params: {
+ risk_score: 40,
+ severity: 'medium',
+ another_param: 'another',
+ },
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
+ .expect(200);
+
+ const response = await supertest.get(
+ `${getUrlPrefix(
+ Spaces.space1.id
+ )}/internal/alerting/rules/_find?filter=alert.attributes.params.risk_score:40`
+ );
+
+ expect(response.status).to.eql(200);
+ objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting');
+ expect(response.body.total).to.equal(1);
+ expect(response.body.data[0].mapped_params).to.eql({
+ risk_score: 40,
+ severity: '40-medium',
+ });
+ });
+
it('should allow providing custom saved object ids (uuid v1)', async () => {
const customId = '09570bb0-6299-11eb-8fde-9fe5ce6ea450';
const response = await supertest
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
index 94198579d612d..7a4a91bd575bb 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
@@ -106,6 +106,24 @@ const findTestUtils = (
createAlert(objectRemover, supertest, { params: { strValue: 'my a' } }),
createAlert(objectRemover, supertest, { params: { strValue: 'my b' } }),
createAlert(objectRemover, supertest, { params: { strValue: 'my c' } }),
+ createAlert(objectRemover, supertest, {
+ params: {
+ risk_score: 60,
+ severity: 'high',
+ },
+ }),
+ createAlert(objectRemover, supertest, {
+ params: {
+ risk_score: 40,
+ severity: 'medium',
+ },
+ }),
+ createAlert(objectRemover, supertest, {
+ params: {
+ risk_score: 20,
+ severity: 'low',
+ },
+ }),
]);
});
@@ -171,6 +189,62 @@ const findTestUtils = (
expect(response.body.total).to.equal(1);
expect(response.body.data[0].params.strValue).to.eql('my b');
});
+
+ it('should sort by parameters', async () => {
+ const response = await supertest.get(
+ `${getUrlPrefix(Spaces.space1.id)}/${
+ describeType === 'public' ? 'api' : 'internal'
+ }/alerting/rules/_find?sort_field=params.severity&sort_order=asc`
+ );
+ expect(response.body.data[0].params.severity).to.equal('low');
+ expect(response.body.data[1].params.severity).to.equal('medium');
+ expect(response.body.data[2].params.severity).to.equal('high');
+ });
+
+ it('should search by parameters', async () => {
+ const response = await supertest.get(
+ `${getUrlPrefix(Spaces.space1.id)}/${
+ describeType === 'public' ? 'api' : 'internal'
+ }/alerting/rules/_find?search_fields=params.severity&search=medium`
+ );
+
+ expect(response.status).to.eql(200);
+ expect(response.body.total).to.equal(1);
+ expect(response.body.data[0].params.severity).to.eql('medium');
+ });
+
+ it('should filter on parameters', async () => {
+ const response = await supertest.get(
+ `${getUrlPrefix(Spaces.space1.id)}/${
+ describeType === 'public' ? 'api' : 'internal'
+ }/alerting/rules/_find?filter=alert.attributes.params.risk_score:40`
+ );
+
+ expect(response.status).to.eql(200);
+ expect(response.body.total).to.equal(1);
+ expect(response.body.data[0].params.risk_score).to.eql(40);
+
+ if (describeType === 'public') {
+ expect(response.body.data[0].mapped_params).to.eql(undefined);
+ }
+ });
+
+ it('should error if filtering on mapped parameters directly using the public API', async () => {
+ const response = await supertest.get(
+ `${getUrlPrefix(Spaces.space1.id)}/${
+ describeType === 'public' ? 'api' : 'internal'
+ }/alerting/rules/_find?filter=alert.attributes.mapped_params.risk_score:40`
+ );
+
+ if (describeType === 'public') {
+ expect(response.status).to.eql(400);
+ expect(response.body.message).to.eql(
+ 'Error find rules: Filter is not supported on this field alert.attributes.mapped_params.risk_score'
+ );
+ } else {
+ expect(response.status).to.eql(200);
+ }
+ });
});
});
};
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
index 5077c8d720c24..9bcce86b57fe6 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
@@ -407,5 +407,21 @@ export default function createGetTests({ getService }: FtrProviderContext) {
'__internal_immutable:false',
]);
});
+
+ it('8.2.0 migrates params to mapped_params for specific params properties', async () => {
+ const response = await es.get<{ alert: RawRule }>(
+ {
+ index: '.kibana',
+ id: 'alert:66560b6f-5ca4-41e2-a1a1-dcfd7117e124',
+ },
+ { meta: true }
+ );
+
+ expect(response.statusCode).to.equal(200);
+ expect(response.body._source?.alert?.mapped_params).to.eql({
+ risk_score: 90,
+ severity: '80-critical',
+ });
+ });
});
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
index 326fb0bfac465..d97ca18c52d4a 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
@@ -32,13 +32,15 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
tags: ['bar'],
params: {
foo: true,
+ risk_score: 40,
+ severity: 'medium',
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
};
- const response = await supertest
+ let response = await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.send(updatedData)
@@ -68,6 +70,17 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
Date.parse(response.body.created_at)
);
+ response = await supertest.get(
+ `${getUrlPrefix(
+ Spaces.space1.id
+ )}/internal/alerting/rules/_find?filter=alert.attributes.params.risk_score:40`
+ );
+
+ expect(response.body.data[0].mapped_params).to.eql({
+ risk_score: 40,
+ severity: '40-medium',
+ });
+
// Ensure AAD isn't broken
await checkAAD({
supertest,
@@ -126,6 +139,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
throttle: '1m',
notifyWhen: 'onThrottleInterval',
};
+
const response = await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json
index 96dad21732d0d..39ce6248c7ebb 100644
--- a/x-pack/test/functional/es_archives/alerts/data.json
+++ b/x-pack/test/functional/es_archives/alerts/data.json
@@ -854,3 +854,45 @@
}
}
}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "alert:66560b6f-5ca4-41e2-a1a1-dcfd7117e124",
+ "index": ".kibana_1",
+ "source": {
+ "alert" : {
+ "name" : "Test mapped params migration",
+ "alertTypeId" : "siem.signals",
+ "consumer" : "alertsFixture",
+ "params" : {
+ "type": "eql",
+ "risk_score": 90,
+ "severity": "critical"
+ },
+ "schedule" : {
+ "interval" : "1m"
+ },
+ "enabled" : true,
+ "actions" : [ ],
+ "throttle" : null,
+ "apiKeyOwner" : null,
+ "apiKey" : null,
+ "createdBy" : "elastic",
+ "updatedBy" : "elastic",
+ "createdAt" : "2021-07-27T20:42:55.896Z",
+ "muteAll" : false,
+ "mutedInstanceIds" : [ ],
+ "scheduledTaskId" : null,
+ "tags": []
+ },
+ "type" : "alert",
+ "migrationVersion" : {
+ "alert" : "7.8.0"
+ },
+ "updated_at" : "2021-08-13T23:00:11.985Z",
+ "references": [
+ ]
+ }
+ }
+}
\ No newline at end of file
From 646c15c1de96931108ce3d36cb64ccec61c1c40a Mon Sep 17 00:00:00 2001
From: Tyler Smalley
Date: Fri, 4 Mar 2022 10:01:56 -0800
Subject: [PATCH 07/27] [Github] Remove Security & Ops project assigner
(#126939)
---
.github/workflows/project-assigner.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml
index 65808dffd801f..8c381dd1ecdef 100644
--- a/.github/workflows/project-assigner.yml
+++ b/.github/workflows/project-assigner.yml
@@ -18,8 +18,6 @@ jobs:
{"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"},
{"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"},
{"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"},
- {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"},
- {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"},
- {"label": "Team:Operations", "projectNumber": 314, "columnName": "Triage", "projectScope": "org"}
+ {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}
]
ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From 377e2b4c3db1d90d7067ce2c6ab161c9554c90b7 Mon Sep 17 00:00:00 2001
From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com>
Date: Fri, 4 Mar 2022 13:17:25 -0500
Subject: [PATCH 08/27] [Security Solution] Improve fields browser performance
(#126114)
* Probably better
* Make backspace not slow
* Type and prop cleanup
* PR comments, fix failing cypress test
* Update cypress tests to wait for debounced text filtering
* Update cypress test
* Update failing cypress tests by waiting when needed
* Reload entire page for field browser tests
* Skip failing local storage test
* Remove unused import, cleanKibana back to before
* Skip failing tests
* Clear applied filter onHide, undo some cypress changes
* Remove unnecessary wait
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../integration/cases/creation.spec.ts | 3 +-
.../detection_rules/export_rule.spec.ts | 3 +-
.../integration/hosts/events_viewer.spec.ts | 1 -
.../timelines/fields_browser.spec.ts | 23 +++-
.../timelines/local_storage.spec.ts | 2 +-
.../cypress/tasks/alerts_detection_rules.ts | 2 +-
.../cypress/tasks/fields_browser.ts | 13 ++-
.../components/fields_browser/index.tsx | 5 +-
.../fields_browser/field_browser.test.tsx | 2 +
.../toolbar/fields_browser/field_browser.tsx | 5 +-
.../toolbar/fields_browser/fields_pane.tsx | 25 +++--
.../t_grid/toolbar/fields_browser/index.tsx | 106 ++++++++++--------
.../es_archives/auditbeat/mappings.json | 5 +
13 files changed, 126 insertions(+), 69 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts
index 90ab1d098aef5..d08f11a95b194 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts
@@ -50,7 +50,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { CASES_URL } from '../../urls/navigation';
-describe('Cases', () => {
+// Flaky: https://github.com/elastic/kibana/issues/69847
+describe.skip('Cases', () => {
beforeEach(() => {
cleanKibana();
createTimeline(getCase1().timeline).then((response) =>
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts
index 0314c0c3a66b6..1e1abaa326bd4 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts
@@ -13,7 +13,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
-describe('Export rules', () => {
+// Flaky https://github.com/elastic/kibana/issues/69849
+describe.skip('Export rules', () => {
beforeEach(() => {
cleanKibana();
cy.intercept(
diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts
index 048efd00d276b..c28c55e0eb3f7 100644
--- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts
@@ -108,7 +108,6 @@ describe('Events Viewer', () => {
it('resets all fields in the events viewer when `Reset Fields` is clicked', () => {
const filterInput = 'host.geo.c';
-
filterFieldsBrowser(filterInput);
cy.get(HOST_GEO_COUNTRY_NAME_HEADER).should('not.exist');
addsHostGeoCountryNameToHeader();
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
index be726f0323d48..07ea4078ce7c4 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
@@ -32,6 +32,7 @@ import {
import { loginAndWaitForPage } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline';
+import { ecsFieldMap } from '../../../../rule_registry/common/assets/field_maps/ecs_field_map';
import { HOSTS_URL } from '../../urls/navigation';
@@ -109,7 +110,27 @@ describe('Fields Browser', () => {
filterFieldsBrowser(filterInput);
- cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '5');
+ const fieldsThatMatchFilterInput = Object.keys(ecsFieldMap).filter((fieldName) => {
+ const dotDelimitedFieldParts = fieldName.split('.');
+ const fieldPartMatch = dotDelimitedFieldParts.filter((fieldPart) => {
+ const camelCasedStringsMatching = fieldPart
+ .split('_')
+ .some((part) => part.startsWith(filterInput));
+ if (fieldPart.startsWith(filterInput)) {
+ return true;
+ } else if (camelCasedStringsMatching) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ return fieldName.startsWith(filterInput) || fieldPartMatch.length > 0;
+ }).length;
+
+ cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should(
+ 'have.text',
+ fieldsThatMatchFilterInput
+ );
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts
index 617f04697c951..b3139d94aa625 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts
@@ -14,7 +14,7 @@ import { waitsForEventsToBeLoaded } from '../../tasks/hosts/events';
import { removeColumn } from '../../tasks/timeline';
// TODO: Fix bug in persisting the columns of timeline
-describe('persistent timeline', () => {
+describe.skip('persistent timeline', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPage(HOSTS_URL);
diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
index 8475ef7247c2c..ab09aca83f575 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
@@ -233,7 +233,7 @@ export const changeRowsPerPageTo = (rowsCount: number) => {
cy.get(PAGINATION_POPOVER_BTN).click({ force: true });
cy.get(rowsPerPageSelector(rowsCount))
.pipe(($el) => $el.trigger('click'))
- .should('not.be.visible');
+ .should('not.exist');
};
export const changeRowsPerPageTo100 = () => {
diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts
index ee8bdb3b023dd..941a19669f2ef 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts
@@ -34,17 +34,24 @@ export const addsHostGeoContinentNameToTimeline = () => {
};
export const clearFieldsBrowser = () => {
+ cy.clock();
cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}');
+ cy.wait(0);
+ cy.tick(1000);
};
export const closeFieldsBrowser = () => {
cy.get(CLOSE_BTN).click({ force: true });
+ cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.exist');
};
export const filterFieldsBrowser = (fieldName: string) => {
- cy.get(FIELDS_BROWSER_FILTER_INPUT)
- .type(fieldName)
- .should('not.have.class', 'euiFieldSearch-isLoading');
+ cy.clock();
+ cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName, { delay: 50 });
+ cy.wait(0);
+ cy.tick(1000);
+ // the text filter is debounced by 250 ms, wait 1s for changes to be applied
+ cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.have.class', 'euiFieldSearch-isLoading');
};
export const removesMessageField = () => {
diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx
index 02fd0553f4016..31b8e9f62803e 100644
--- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx
+++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx
@@ -28,10 +28,7 @@ export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponent
return (
-
+
);
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
index dc9837007e153..d435d7a280840 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
@@ -32,6 +32,7 @@ const testProps = {
browserFields: mockBrowserFields,
filteredBrowserFields: mockBrowserFields,
searchInput: '',
+ appliedFilterInput: '',
isSearching: false,
onCategorySelected: jest.fn(),
onHide,
@@ -84,6 +85,7 @@ describe('FieldsBrowser', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
searchInput={''}
+ appliedFilterInput={''}
isSearching={false}
onCategorySelected={jest.fn()}
onHide={jest.fn()}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
index fea22e4efe77c..e55f54e946ad1 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
@@ -75,6 +75,8 @@ type Props = Pick &
isSearching: boolean;
/** The text displayed in the search input */
searchInput: string;
+ /** The text actually being applied to the result set, a debounced version of searchInput */
+ appliedFilterInput: string;
/**
* The category selected on the left-hand side of the field browser
*/
@@ -115,6 +117,7 @@ const FieldsBrowserComponent: React.FC = ({
onHide,
restoreFocusTo,
searchInput,
+ appliedFilterInput,
selectedCategoryId,
timelineId,
width = FIELD_BROWSER_WIDTH,
@@ -237,7 +240,7 @@ const FieldsBrowserComponent: React.FC = ({
filteredBrowserFields={filteredBrowserFields}
onCategorySelected={onCategorySelected}
onUpdateColumns={onUpdateColumns}
- searchInput={searchInput}
+ searchInput={appliedFilterInput}
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
width={FIELDS_PANE_WIDTH}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
index 5345475a02501..d1d0254d0c917 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
@@ -98,19 +98,30 @@ export const FieldsPane = React.memo(
[filteredBrowserFields]
);
+ const fieldItems = useMemo(() => {
+ return getFieldItems({
+ category: filteredBrowserFields[selectedCategoryId],
+ columnHeaders,
+ highlight: searchInput,
+ timelineId,
+ toggleColumn,
+ });
+ }, [
+ columnHeaders,
+ filteredBrowserFields,
+ searchInput,
+ selectedCategoryId,
+ timelineId,
+ toggleColumn,
+ ]);
+
if (filteredBrowserFieldsExists) {
return (
= ({
/** all field names shown in the field browser must contain this string (when specified) */
const [filterInput, setFilterInput] = useState('');
+
+ const [appliedFilterInput, setAppliedFilterInput] = useState('');
/** all fields in this collection have field names that match the filterInput */
const [filteredBrowserFields, setFilteredBrowserFields] = useState(null);
/** when true, show a spinner in the input to indicate the field browser is searching for matching field names */
@@ -51,15 +53,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({
const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME);
/** show the field browser */
const [show, setShow] = useState(false);
- useEffect(() => {
- return () => {
- if (inputTimeoutId.current !== 0) {
- // ⚠️ mutation: cancel any remaining timers and zero-out the timer id:
- clearTimeout(inputTimeoutId.current);
- inputTimeoutId.current = 0;
- }
- };
- }, []);
/** Shows / hides the field browser */
const onShow = useCallback(() => {
@@ -69,52 +62,68 @@ export const StatefulFieldsBrowserComponent: React.FC = ({
/** Invoked when the field browser should be hidden */
const onHide = useCallback(() => {
setFilterInput('');
+ setAppliedFilterInput('');
setFilteredBrowserFields(null);
setIsSearching(false);
setSelectedCategoryId(DEFAULT_CATEGORY_NAME);
setShow(false);
}, []);
+ const newFilteredBrowserFields = useMemo(() => {
+ return filterBrowserFieldsByFieldName({
+ browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
+ substring: appliedFilterInput,
+ });
+ }, [appliedFilterInput, browserFields]);
+
+ const newSelectedCategoryId = useMemo(() => {
+ if (appliedFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0) {
+ return DEFAULT_CATEGORY_NAME;
+ } else {
+ return Object.keys(newFilteredBrowserFields)
+ .sort()
+ .reduce((selected, category) => {
+ const filteredBrowserFieldsByCategory =
+ (newFilteredBrowserFields[category] && newFilteredBrowserFields[category].fields) || [];
+ const filteredBrowserFieldsBySelected =
+ (newFilteredBrowserFields[selected] && newFilteredBrowserFields[selected].fields) || [];
+ return newFilteredBrowserFields[category].fields != null &&
+ newFilteredBrowserFields[selected].fields != null &&
+ Object.keys(filteredBrowserFieldsByCategory).length >
+ Object.keys(filteredBrowserFieldsBySelected).length
+ ? category
+ : selected;
+ }, Object.keys(newFilteredBrowserFields)[0]);
+ }
+ }, [appliedFilterInput, newFilteredBrowserFields]);
+
/** Invoked when the user types in the filter input */
- const updateFilter = useCallback(
- (newFilterInput: string) => {
- setFilterInput(newFilterInput);
- setIsSearching(true);
- if (inputTimeoutId.current !== 0) {
- clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
- }
- // ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
- inputTimeoutId.current = window.setTimeout(() => {
- const newFilteredBrowserFields = filterBrowserFieldsByFieldName({
- browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
- substring: newFilterInput,
- });
- setFilteredBrowserFields(newFilteredBrowserFields);
- setIsSearching(false);
-
- const newSelectedCategoryId =
- newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0
- ? DEFAULT_CATEGORY_NAME
- : Object.keys(newFilteredBrowserFields)
- .sort()
- .reduce(
- (selected, category) =>
- newFilteredBrowserFields[category].fields != null &&
- newFilteredBrowserFields[selected].fields != null &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Object.keys(newFilteredBrowserFields[category].fields!).length >
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Object.keys(newFilteredBrowserFields[selected].fields!).length
- ? category
- : selected,
- Object.keys(newFilteredBrowserFields)[0]
- );
- setSelectedCategoryId(newSelectedCategoryId);
- }, INPUT_TIMEOUT);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [browserFields, filterInput, inputTimeoutId.current]
- );
+ const updateFilter = useCallback((newFilterInput: string) => {
+ setFilterInput(newFilterInput);
+ setIsSearching(true);
+ }, []);
+
+ useEffect(() => {
+ if (inputTimeoutId.current !== 0) {
+ clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
+ }
+ // ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
+ inputTimeoutId.current = window.setTimeout(() => {
+ setIsSearching(false);
+ setAppliedFilterInput(filterInput);
+ }, INPUT_TIMEOUT);
+ return () => {
+ clearTimeout(inputTimeoutId.current);
+ };
+ }, [filterInput]);
+
+ useEffect(() => {
+ setFilteredBrowserFields(newFilteredBrowserFields);
+ }, [newFilteredBrowserFields]);
+
+ useEffect(() => {
+ setSelectedCategoryId(newSelectedCategoryId);
+ }, [newSelectedCategoryId]);
// only merge in the default category if the field browser is visible
const browserFieldsWithDefaultCategory = useMemo(() => {
@@ -152,6 +161,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({
onSearchInputChange={updateFilter}
restoreFocusTo={customizeColumnsButtonRef}
searchInput={filterInput}
+ appliedFilterInput={appliedFilterInput}
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
width={width}
diff --git a/x-pack/test/security_solution_cypress/es_archives/auditbeat/mappings.json b/x-pack/test/security_solution_cypress/es_archives/auditbeat/mappings.json
index 3196232e59643..061748d72b77b 100644
--- a/x-pack/test/security_solution_cypress/es_archives/auditbeat/mappings.json
+++ b/x-pack/test/security_solution_cypress/es_archives/auditbeat/mappings.json
@@ -1735,6 +1735,10 @@
"ignore_above": 1024,
"type": "keyword"
},
+ "continent_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
"continent_name": {
"ignore_above": 1024,
"type": "keyword"
@@ -3110,6 +3114,7 @@
"group.name",
"host.architecture",
"host.geo.city_name",
+ "host.geo.continent_code",
"host.geo.continent_name",
"host.geo.country_iso_code",
"host.geo.country_name",
From eed64fda745de983c7a21f7b291952fcb396ddd8 Mon Sep 17 00:00:00 2001
From: Jason Rhodes
Date: Fri, 4 Mar 2022 14:17:01 -0500
Subject: [PATCH 09/27] Testing project_assigner action for beta projects
(#126950)
We suspect this action will not work with GH beta projects, so let's confirm.
---
.github/workflows/assign-infra-monitoring.yml | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 .github/workflows/assign-infra-monitoring.yml
diff --git a/.github/workflows/assign-infra-monitoring.yml b/.github/workflows/assign-infra-monitoring.yml
new file mode 100644
index 0000000000000..96376ecebf854
--- /dev/null
+++ b/.github/workflows/assign-infra-monitoring.yml
@@ -0,0 +1,22 @@
+name: Assign to Infra Monitoring UI (beta) project
+on:
+ issues:
+ types: [labeled]
+
+jobs:
+ assign_to_project:
+ runs-on: ubuntu-latest
+ name: Assign issue or PR to project based on label
+ steps:
+ - name: Assign to project
+ uses: elastic/github-actions/project-assigner@v2.1.1
+ id: project_assigner_infra_monitoring
+ with:
+ issue-mappings: |
+ [
+ {"label": "Team:Infra Monitoring UI", "projectNumber": 664, "projectScope": "org"},
+ {"label": "Feature:Stack Monitoring", "projectNumber": 664, "projectScope": "org"},
+ {"label": "Feature:Logs UI", "projectNumber": 664, "projectScope": "org"},
+ {"label": "Feature:Metrics UI", "projectNumber": 664, "projectScope": "org"},
+ ]
+ ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From 70a4f7930f007af741f0328c88c9a6a714542135 Mon Sep 17 00:00:00 2001
From: Tyler Smalley
Date: Fri, 4 Mar 2022 11:19:58 -0800
Subject: [PATCH 10/27] Revert "Testing project_assigner action for beta
projects" (#126952)
This reverts commit eed64fda745de983c7a21f7b291952fcb396ddd8.
---
.github/workflows/assign-infra-monitoring.yml | 22 -------------------
1 file changed, 22 deletions(-)
delete mode 100644 .github/workflows/assign-infra-monitoring.yml
diff --git a/.github/workflows/assign-infra-monitoring.yml b/.github/workflows/assign-infra-monitoring.yml
deleted file mode 100644
index 96376ecebf854..0000000000000
--- a/.github/workflows/assign-infra-monitoring.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Assign to Infra Monitoring UI (beta) project
-on:
- issues:
- types: [labeled]
-
-jobs:
- assign_to_project:
- runs-on: ubuntu-latest
- name: Assign issue or PR to project based on label
- steps:
- - name: Assign to project
- uses: elastic/github-actions/project-assigner@v2.1.1
- id: project_assigner_infra_monitoring
- with:
- issue-mappings: |
- [
- {"label": "Team:Infra Monitoring UI", "projectNumber": 664, "projectScope": "org"},
- {"label": "Feature:Stack Monitoring", "projectNumber": 664, "projectScope": "org"},
- {"label": "Feature:Logs UI", "projectNumber": 664, "projectScope": "org"},
- {"label": "Feature:Metrics UI", "projectNumber": 664, "projectScope": "org"},
- ]
- ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From 1ed4aea9e4be3a50d1635552af005b4ec76b8ebc Mon Sep 17 00:00:00 2001
From: Jason Rhodes
Date: Fri, 4 Mar 2022 14:23:35 -0500
Subject: [PATCH 11/27] Adds workflow for infra monitoring ui team (#126921)
* Adds workflow for infra monitoring ui team
* Adds other labels for our team
* Updates token to general use one
@tylersmalley mentioned this one exists, so it seems like a safer choice for now. Ultimately we may want a single one from the Elastic org that is enabled for every repo that needs it.
---
.../workflows/project-infra-monitoring-ui.yml | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 .github/workflows/project-infra-monitoring-ui.yml
diff --git a/.github/workflows/project-infra-monitoring-ui.yml b/.github/workflows/project-infra-monitoring-ui.yml
new file mode 100644
index 0000000000000..b9fd04b164a8d
--- /dev/null
+++ b/.github/workflows/project-infra-monitoring-ui.yml
@@ -0,0 +1,25 @@
+name: Add issues to Infra Monitoring UI project
+on:
+ issues:
+ types: [labeled]
+
+jobs:
+ sync_issues_with_table:
+ runs-on: ubuntu-latest
+ name: Add issues to project
+ steps:
+ - name: Add
+ uses: richkuz/projectnext-label-assigner@1.0.2
+ id: add_to_projects
+ with:
+ config: |
+ [
+ {"label": "Team:Infra Monitoring UI", "projectNumber": 664},
+ {"label": "Feature:Stack Monitoring", "projectNumber": 664},
+ {"label": "Feature:Logs UI", "projectNumber": 664},
+ {"label": "Feature:Metrics UI", "projectNumber": 664},
+ ]
+ env:
+ GRAPHQL_API_BASE: 'https://api.github.com'
+ PAT_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
From f974150c7be332ed9e016f1134400c268ffe1cde Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Fri, 4 Mar 2022 13:46:45 -0700
Subject: [PATCH 12/27] [Dashboard] Close toolbar popover for log stream
visualizations (#126840)
* Fix close popover on click
* Fix close popover on click - second attempt
* Add functional test to ensure menu closes
---
.../application/top_nav/editor_menu.tsx | 85 ++++++++++---------
.../dashboard/create_and_add_embeddables.ts | 16 +++-
.../services/dashboard/add_panel.ts | 5 ++
3 files changed, 65 insertions(+), 41 deletions(-)
diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
index 44b1aec226fd6..5fece7ff959ce 100644
--- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
@@ -153,7 +153,8 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
};
const getEmbeddableFactoryMenuItem = (
- factory: EmbeddableFactoryDefinition
+ factory: EmbeddableFactoryDefinition,
+ closePopover: () => void
): EuiContextMenuPanelItemDescriptor => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
@@ -164,6 +165,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
icon,
toolTipContent,
onClick: async () => {
+ closePopover();
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, factory.type);
}
@@ -192,42 +194,47 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
defaultMessage: 'Aggregation based',
});
- const editorMenuPanels = [
- {
- id: 0,
- items: [
- ...visTypeAliases.map(getVisTypeAliasMenuItem),
- ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
- name: appName,
- icon,
- panel: panelId,
- 'data-test-subj': `dashboardEditorMenu-${id}Group`,
- })),
- ...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
- ...promotedVisTypes.map(getVisTypeMenuItem),
- {
- name: aggsPanelTitle,
- icon: 'visualizeApp',
- panel: aggBasedPanelID,
- 'data-test-subj': `dashboardEditorAggBasedMenuItem`,
- },
- ...toolVisTypes.map(getVisTypeMenuItem),
- ],
- },
- {
- id: aggBasedPanelID,
- title: aggsPanelTitle,
- items: aggsBasedVisTypes.map(getVisTypeMenuItem),
- },
- ...Object.values(factoryGroupMap).map(
- ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
- id: panelId,
- title: appName,
- items: groupFactories.map(getEmbeddableFactoryMenuItem),
- })
- ),
- ];
-
+ const getEditorMenuPanels = (closePopover: () => void) => {
+ return [
+ {
+ id: 0,
+ items: [
+ ...visTypeAliases.map(getVisTypeAliasMenuItem),
+ ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
+ name: appName,
+ icon,
+ panel: panelId,
+ 'data-test-subj': `dashboardEditorMenu-${id}Group`,
+ })),
+ ...ungroupedFactories.map((factory) => {
+ return getEmbeddableFactoryMenuItem(factory, closePopover);
+ }),
+ ...promotedVisTypes.map(getVisTypeMenuItem),
+ {
+ name: aggsPanelTitle,
+ icon: 'visualizeApp',
+ panel: aggBasedPanelID,
+ 'data-test-subj': `dashboardEditorAggBasedMenuItem`,
+ },
+ ...toolVisTypes.map(getVisTypeMenuItem),
+ ],
+ },
+ {
+ id: aggBasedPanelID,
+ title: aggsPanelTitle,
+ items: aggsBasedVisTypes.map(getVisTypeMenuItem),
+ },
+ ...Object.values(factoryGroupMap).map(
+ ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
+ id: panelId,
+ title: appName,
+ items: groupFactories.map((factory) => {
+ return getEmbeddableFactoryMenuItem(factory, closePopover);
+ }),
+ })
+ ),
+ ];
+ };
return (
{
panelPaddingSize="none"
data-test-subj="dashboardEditorMenuButton"
>
- {() => (
+ {({ closePopover }: { closePopover: () => void }) => (
{
await PageObjects.common.navigateToApp('dashboard');
- await PageObjects.dashboard.preserveCrossAppState();
- await PageObjects.dashboard.loadSavedDashboard('few panels');
+ await PageObjects.dashboard.clickNewDashboard();
+ await PageObjects.dashboard.switchToEditMode();
+ await dashboardAddPanel.clickEditorMenuButton();
+ await dashboardAddPanel.clickAddNewEmbeddableLink('LOG_STREAM_EMBEDDABLE');
+ await dashboardAddPanel.expectEditorMenuClosed();
});
describe('add new visualization link', () => {
+ before(async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.preserveCrossAppState();
+ await PageObjects.dashboard.loadSavedDashboard('few panels');
+ });
+
it('adds new visualization via the top nav link', async () => {
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
await PageObjects.dashboard.switchToEditMode();
diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts
index 43ab1f966bc9a..e42c221a49475 100644
--- a/test/functional/services/dashboard/add_panel.ts
+++ b/test/functional/services/dashboard/add_panel.ts
@@ -46,6 +46,11 @@ export class DashboardAddPanelService extends FtrService {
async clickEditorMenuButton() {
this.log.debug('DashboardAddPanel.clickEditorMenuButton');
await this.testSubjects.click('dashboardEditorMenuButton');
+ await this.testSubjects.existOrFail('dashboardEditorContextMenu');
+ }
+
+ async expectEditorMenuClosed() {
+ await this.testSubjects.missingOrFail('dashboardEditorContextMenu');
}
async clickAggBasedVisualizations() {
From 5f8f4d7c4f9151d4baaa5cb579e1c732b57b078a Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Fri, 4 Mar 2022 12:56:30 -0800
Subject: [PATCH 13/27] Edits dateFormat settings (#126858)
---
.../server/ui_settings/settings/date_formats.ts | 14 +++++---------
.../plugins/translations/translations/fr-FR.json | 5 -----
.../plugins/translations/translations/ja-JP.json | 5 -----
.../plugins/translations/translations/zh-CN.json | 5 -----
4 files changed, 5 insertions(+), 24 deletions(-)
diff --git a/src/core/server/ui_settings/settings/date_formats.ts b/src/core/server/ui_settings/settings/date_formats.ts
index c626c4a83cc4c..039ead326a236 100644
--- a/src/core/server/ui_settings/settings/date_formats.ts
+++ b/src/core/server/ui_settings/settings/date_formats.ts
@@ -31,7 +31,7 @@ export const getDateFormatSettings = (): Record => {
}),
value: 'MMM D, YYYY @ HH:mm:ss.SSS',
description: i18n.translate('core.ui_settings.params.dateFormatText', {
- defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}',
+ defaultMessage: 'The {formatLink} for pretty formatted dates.',
description:
'Part of composite text: core.ui_settings.params.dateFormatText + ' +
'core.ui_settings.params.dateFormat.optionsLinkText',
@@ -48,15 +48,11 @@ export const getDateFormatSettings = (): Record => {
},
'dateFormat:tz': {
name: i18n.translate('core.ui_settings.params.dateFormat.timezoneTitle', {
- defaultMessage: 'Timezone for date formatting',
+ defaultMessage: 'Time zone',
}),
value: 'Browser',
description: i18n.translate('core.ui_settings.params.dateFormat.timezoneText', {
- defaultMessage:
- 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.',
- values: {
- defaultOption: '"Browser"',
- },
+ defaultMessage: 'The default time zone.',
}),
type: 'select',
options: timezones,
@@ -115,7 +111,7 @@ export const getDateFormatSettings = (): Record => {
}),
value: defaultWeekday,
description: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekText', {
- defaultMessage: 'What day should weeks start on?',
+ defaultMessage: 'The day that starts the week.',
}),
type: 'select',
options: weekdays,
@@ -141,7 +137,7 @@ export const getDateFormatSettings = (): Record => {
}),
value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS',
description: i18n.translate('core.ui_settings.params.dateNanosFormatText', {
- defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch',
+ defaultMessage: 'The format for {dateNanosLink} data.',
values: {
dateNanosLink:
'' +
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 96327533d4e29..99e4c6a4ca1f3 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -1254,18 +1254,13 @@
"core.toasts.errorToast.seeFullError": "Voir l'erreur en intégralité",
"core.ui_settings.params.darkModeText": "Activez le mode sombre pour l'interface utilisateur Kibana. Vous devez actualiser la page pour que ce paramètre s’applique.",
"core.ui_settings.params.darkModeTitle": "Mode sombre",
- "core.ui_settings.params.dateFormat.dayOfWeekText": "Quel est le premier jour de la semaine ?",
"core.ui_settings.params.dateFormat.dayOfWeekTitle": "Jour de la semaine",
"core.ui_settings.params.dateFormat.optionsLinkText": "format",
"core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "Intervalles ISO8601",
"core.ui_settings.params.dateFormat.scaledText": "Les valeurs qui définissent le format utilisé lorsque les données temporelles sont rendues dans l'ordre, et lorsque les horodatages formatés doivent s'adapter à l'intervalle entre les mesures. Les clés sont {intervalsLink}.",
"core.ui_settings.params.dateFormat.scaledTitle": "Format de date scalé",
"core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "Fuseau horaire non valide : {timezone}",
- "core.ui_settings.params.dateFormat.timezoneText": "Fuseau horaire à utiliser. L’option {defaultOption} utilise le fuseau horaire détecté par le navigateur.",
- "core.ui_settings.params.dateFormat.timezoneTitle": "Fuseau horaire pour le format de date",
- "core.ui_settings.params.dateFormatText": "{formatLink} utilisé pour les dates formatées",
"core.ui_settings.params.dateFormatTitle": "Format de date",
- "core.ui_settings.params.dateNanosFormatText": "Utilisé pour le type de données {dateNanosLink} d'Elasticsearch",
"core.ui_settings.params.dateNanosFormatTitle": "Date au format nanosecondes",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "Jour de la semaine non valide : {dayOfWeek}",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index d47ff1ed31496..70d65550a4056 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -1567,18 +1567,13 @@
"core.toasts.errorToast.seeFullError": "完全なエラーを表示",
"core.ui_settings.params.darkModeText": "Kibana UIのダークモードを有効にします。この設定を適用するにはページの更新が必要です。",
"core.ui_settings.params.darkModeTitle": "ダークモード",
- "core.ui_settings.params.dateFormat.dayOfWeekText": "週の初めの曜日を設定します",
"core.ui_settings.params.dateFormat.dayOfWeekTitle": "曜日",
"core.ui_settings.params.dateFormat.optionsLinkText": "フォーマット",
"core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601間隔",
"core.ui_settings.params.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは{intervalsLink}です。",
"core.ui_settings.params.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです",
"core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "無効なタイムゾーン:{timezone}",
- "core.ui_settings.params.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption}ではご使用のブラウザーにより検知されたタイムゾーンが使用されます。",
- "core.ui_settings.params.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン",
- "core.ui_settings.params.dateFormatText": "きちんとフォーマットされたデータを表示する際、この{formatLink}を使用します",
"core.ui_settings.params.dateFormatTitle": "データフォーマット",
- "core.ui_settings.params.dateNanosFormatText": "Elasticsearchの{dateNanosLink}データタイプに使用されます",
"core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "無効な曜日:{dayOfWeek}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 96f9e892ba0b9..90eb0d3c35f65 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1574,18 +1574,13 @@
"core.toasts.errorToast.seeFullError": "请参阅完整的错误信息",
"core.ui_settings.params.darkModeText": "对 Kibana UI 启用深色模式。需要刷新页面,才能应用设置。",
"core.ui_settings.params.darkModeTitle": "深色模式",
- "core.ui_settings.params.dateFormat.dayOfWeekText": "一周应该从哪一天开始?",
"core.ui_settings.params.dateFormat.dayOfWeekTitle": "周内日",
"core.ui_settings.params.dateFormat.optionsLinkText": "格式",
"core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔",
"core.ui_settings.params.dateFormat.scaledText": "定义在以下场合中采用的格式的值:基于时间的数据按顺序呈现,且经格式化的时间戳应适应度量之间的时间间隔。键是{intervalsLink}。",
"core.ui_settings.params.dateFormat.scaledTitle": "标度日期格式",
"core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "时区无效:{timezone}",
- "core.ui_settings.params.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。",
- "core.ui_settings.params.dateFormat.timezoneTitle": "用于设置日期格式的时区",
- "core.ui_settings.params.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}",
"core.ui_settings.params.dateFormatTitle": "日期格式",
- "core.ui_settings.params.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型",
"core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "周内日无效:{dayOfWeek}",
From 9514e6be38e2f17c3710f17651e716ce94ec9613 Mon Sep 17 00:00:00 2001
From: Jason Rhodes
Date: Fri, 4 Mar 2022 16:07:45 -0500
Subject: [PATCH 14/27] Update and rename project-infra-monitoring-ui.yml to
add-to-imui-project.yml (#126963)
The previous action was failing with an obscure JSON error, so I've copied the APM and Fleet actions instead.
---
.github/workflows/add-to-imui-project.yml | 31 +++++++++++++++++++
.../workflows/project-infra-monitoring-ui.yml | 25 ---------------
2 files changed, 31 insertions(+), 25 deletions(-)
create mode 100644 .github/workflows/add-to-imui-project.yml
delete mode 100644 .github/workflows/project-infra-monitoring-ui.yml
diff --git a/.github/workflows/add-to-imui-project.yml b/.github/workflows/add-to-imui-project.yml
new file mode 100644
index 0000000000000..3cf120b2e81bc
--- /dev/null
+++ b/.github/workflows/add-to-imui-project.yml
@@ -0,0 +1,31 @@
+name: Add to Infra Monitoring UI project
+on:
+ issues:
+ types:
+ - labeled
+jobs:
+ add_to_project:
+ runs-on: ubuntu-latest
+ if: |
+ contains(github.event.issue.labels.*.name, 'Team:Infra Monitoring UI') ||
+ contains(github.event.issue.labels.*.name, 'Feature:Stack Monitoring') ||
+ contains(github.event.issue.labels.*.name, 'Feature:Logs UI') ||
+ contains(github.event.issue.labels.*.name, 'Feature:Metrics UI')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:ID!,$contentid:ID!) {
+ addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
+ projectNextItem {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PN_kwDOAGc3Zs1EEA"
+ GITHUB_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
diff --git a/.github/workflows/project-infra-monitoring-ui.yml b/.github/workflows/project-infra-monitoring-ui.yml
deleted file mode 100644
index b9fd04b164a8d..0000000000000
--- a/.github/workflows/project-infra-monitoring-ui.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Add issues to Infra Monitoring UI project
-on:
- issues:
- types: [labeled]
-
-jobs:
- sync_issues_with_table:
- runs-on: ubuntu-latest
- name: Add issues to project
- steps:
- - name: Add
- uses: richkuz/projectnext-label-assigner@1.0.2
- id: add_to_projects
- with:
- config: |
- [
- {"label": "Team:Infra Monitoring UI", "projectNumber": 664},
- {"label": "Feature:Stack Monitoring", "projectNumber": 664},
- {"label": "Feature:Logs UI", "projectNumber": 664},
- {"label": "Feature:Metrics UI", "projectNumber": 664},
- ]
- env:
- GRAPHQL_API_BASE: 'https://api.github.com'
- PAT_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
From 63892cf654fccf56a80a01555190fde84f8e46f1 Mon Sep 17 00:00:00 2001
From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com>
Date: Fri, 4 Mar 2022 22:59:08 +0100
Subject: [PATCH 15/27] [Alerting] Paginated http requests for the expensive
queries (#126111)
* Fetch index patterns as chunks on indices search input change.
Use debounce for the http requests that are triggered on input change
---
.../components/index_select_popover.test.tsx | 8 +
.../components/index_select_popover.tsx | 21 +--
.../es_index/es_index_connector.test.tsx | 67 ++++++--
.../es_index/es_index_connector.tsx | 27 ++--
.../public/common/index_controls/index.ts | 38 ++---
.../public/common/lib/data_apis.test.ts | 148 ++++++++++++++++++
.../public/common/lib/data_apis.ts | 82 ++++++++--
7 files changed, 304 insertions(+), 87 deletions(-)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
index e5c8343fddf6d..7b27167d5f5f9 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
@@ -11,6 +11,14 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { IndexSelectPopover } from './index_select_popover';
import { EuiComboBox } from '@elastic/eui';
+jest.mock('lodash', () => {
+ const module = jest.requireActual('lodash');
+ return {
+ ...module,
+ debounce: (fn: () => unknown) => fn,
+ };
+});
+
jest.mock('../../../../triggers_actions_ui/public', () => {
const original = jest.requireActual('../../../../triggers_actions_ui/public');
return {
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx
index fbfb296c7b270..a8b9f3f56dd06 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx
@@ -7,7 +7,7 @@
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { isString } from 'lodash';
+import { isString, debounce } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButtonIcon,
@@ -27,7 +27,6 @@ import {
firstFieldOption,
getFields,
getIndexOptions,
- getIndexPatterns,
getTimeFieldOptions,
IErrorObject,
} from '../../../../triggers_actions_ui/public';
@@ -62,16 +61,14 @@ export const IndexSelectPopover: React.FunctionComponent = ({
const [indexPopoverOpen, setIndexPopoverOpen] = useState(false);
const [indexOptions, setIndexOptions] = useState([]);
- const [indexPatterns, setIndexPatterns] = useState([]);
const [areIndicesLoading, setAreIndicesLoading] = useState(false);
const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]);
- useEffect(() => {
- const indexPatternsFunction = async () => {
- setIndexPatterns(await getIndexPatterns());
- };
- indexPatternsFunction();
- }, []);
+ const loadIndexOptions = debounce(async (search: string) => {
+ setAreIndicesLoading(true);
+ setIndexOptions(await getIndexOptions(http!, search));
+ setAreIndicesLoading(false);
+ }, 250);
useEffect(() => {
const timeFields = getTimeFieldOptions(esFields);
@@ -193,11 +190,7 @@ export const IndexSelectPopover: React.FunctionComponent = ({
setTimeFieldOptions([firstFieldOption, ...timeFields]);
}
}}
- onSearchChange={async (search) => {
- setAreIndicesLoading(true);
- setIndexOptions(await getIndexOptions(http!, search, indexPatterns));
- setAreIndicesLoading(false);
- }}
+ onSearchChange={loadIndexOptions}
onBlur={() => {
if (!index) {
onIndexChange([]);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
index 6aa7fde6d23e1..2f6cbabc676cb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
@@ -11,28 +11,32 @@ import { act } from 'react-dom/test-utils';
import { EsIndexActionConnector } from '../types';
import IndexActionConnectorFields from './es_index_connector';
import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui';
+import { screen, render, fireEvent } from '@testing-library/react';
jest.mock('../../../../common/lib/kibana');
+jest.mock('lodash', () => {
+ const module = jest.requireActual('lodash');
+ return {
+ ...module,
+ debounce: (fn: () => unknown) => fn,
+ };
+});
+
jest.mock('../../../../common/index_controls', () => ({
firstFieldOption: jest.fn(),
getFields: jest.fn(),
getIndexOptions: jest.fn(),
- getIndexPatterns: jest.fn(),
}));
-const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls');
-getIndexPatterns.mockResolvedValueOnce([
- {
- id: 'indexPattern1',
- attributes: {
- title: 'indexPattern1',
- },
- },
+const { getIndexOptions } = jest.requireMock('../../../../common/index_controls');
+
+getIndexOptions.mockResolvedValueOnce([
{
- id: 'indexPattern2',
- attributes: {
- title: 'indexPattern2',
- },
+ label: 'indexOption',
+ options: [
+ { label: 'indexPattern1', value: 'indexPattern1' },
+ { label: 'indexPattern2', value: 'indexPattern2' },
+ ],
},
]);
@@ -59,6 +63,7 @@ function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) {
},
]);
}
+
describe('IndexActionConnectorFields renders', () => {
test('renders correctly when creating connector', async () => {
const props = {
@@ -281,4 +286,40 @@ describe('IndexActionConnectorFields renders', () => {
.filter('[data-test-subj="executionTimeFieldSelect"]');
expect(timeFieldSelect.prop('value')).toEqual('test1');
});
+
+ test('fetches index names on index combobox input change', async () => {
+ const mockIndexName = 'test-index';
+ const props = {
+ action: {
+ actionTypeId: '.index',
+ config: {},
+ secrets: {},
+ } as EsIndexActionConnector,
+ editActionConfig: () => {},
+ editActionSecrets: () => {},
+ errors: { index: [] },
+ readOnly: false,
+ setCallbacks: () => {},
+ isEdit: false,
+ };
+ render();
+
+ const indexComboBox = await screen.findByTestId('connectorIndexesComboBox');
+
+ // time field switch should show up if index has date type field mapping
+ setupGetFieldsResponse(true);
+
+ fireEvent.click(indexComboBox);
+
+ await act(async () => {
+ const event = { target: { value: mockIndexName } };
+ fireEvent.change(screen.getByRole('textbox'), event);
+ });
+
+ expect(getIndexOptions).toHaveBeenCalledTimes(1);
+ expect(getIndexOptions).toHaveBeenCalledWith(expect.anything(), mockIndexName);
+ expect(await screen.findAllByRole('option')).toHaveLength(2);
+ expect(screen.getByText('indexPattern1')).toBeInTheDocument();
+ expect(screen.getByText('indexPattern2')).toBeInTheDocument();
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
index c99477bfa83f9..7b0515d8904e2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
@@ -19,15 +19,11 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
+import { debounce } from 'lodash';
import { ActionConnectorFieldsProps } from '../../../../types';
import { EsIndexActionConnector } from '.././types';
import { getTimeFieldOptions } from '../../../../common/lib/get_time_options';
-import {
- firstFieldOption,
- getFields,
- getIndexOptions,
- getIndexPatterns,
-} from '../../../../common/index_controls';
+import { firstFieldOption, getFields, getIndexOptions } from '../../../../common/index_controls';
import { useKibana } from '../../../../common/lib/kibana';
interface TimeFieldOptions {
@@ -47,10 +43,9 @@ const IndexActionConnectorFields: React.FunctionComponent<
executionTimeField != null
);
- const [indexPatterns, setIndexPatterns] = useState([]);
const [indexOptions, setIndexOptions] = useState([]);
const [timeFieldOptions, setTimeFieldOptions] = useState([]);
- const [isIndiciesLoading, setIsIndiciesLoading] = useState(false);
+ const [areIndiciesLoading, setAreIndicesLoading] = useState(false);
const setTimeFields = (fields: TimeFieldOptions[]) => {
if (fields.length > 0) {
@@ -63,9 +58,14 @@ const IndexActionConnectorFields: React.FunctionComponent<
}
};
+ const loadIndexOptions = debounce(async (search: string) => {
+ setAreIndicesLoading(true);
+ setIndexOptions(await getIndexOptions(http!, search));
+ setAreIndicesLoading(false);
+ }, 250);
+
useEffect(() => {
const indexPatternsFunction = async () => {
- setIndexPatterns(await getIndexPatterns());
if (index) {
const currentEsFields = await getFields(http!, [index]);
setTimeFields(getTimeFieldOptions(currentEsFields as any));
@@ -119,11 +119,12 @@ const IndexActionConnectorFields: React.FunctionComponent<
fullWidth
singleSelection={{ asPlainText: true }}
async
- isLoading={isIndiciesLoading}
+ isLoading={areIndiciesLoading}
isInvalid={isIndexInvalid}
noSuggestions={!indexOptions.length}
options={indexOptions}
data-test-subj="connectorIndexesComboBox"
+ data-testid="connectorIndexesComboBox"
selectedOptions={
index
? [
@@ -147,11 +148,7 @@ const IndexActionConnectorFields: React.FunctionComponent<
const currentEsFields = await getFields(http!, indices);
setTimeFields(getTimeFieldOptions(currentEsFields as any));
}}
- onSearchChange={async (search) => {
- setIsIndiciesLoading(true);
- setIndexOptions(await getIndexOptions(http!, search, indexPatterns));
- setIsIndiciesLoading(false);
- }}
+ onSearchChange={loadIndexOptions}
onBlur={() => {
if (!index) {
editActionConfig('index', '');
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts
index 072684de68b3e..b05c3f51de4ab 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts
@@ -8,48 +8,30 @@
import { uniq } from 'lodash';
import { HttpSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
-import {
- loadIndexPatterns,
- getMatchingIndices,
- getESIndexFields,
- getSavedObjectsClient,
-} from '../lib/data_apis';
+import { loadIndexPatterns, getMatchingIndices, getESIndexFields } from '../lib/data_apis';
export interface IOption {
label: string;
options: Array<{ value: string; label: string }>;
}
-export const getIndexPatterns = async () => {
- // TODO: Implement a possibility to retrive index patterns different way to be able to expose this in consumer plugins
- if (getSavedObjectsClient()) {
- const indexPatternObjects = await loadIndexPatterns();
- return indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title);
- }
- return [];
-};
-
-export const getIndexOptions = async (
- http: HttpSetup,
- pattern: string,
- indexPatternsParam: string[]
-) => {
+export const getIndexOptions = async (http: HttpSetup, pattern: string) => {
const options: IOption[] = [];
if (!pattern) {
return options;
}
- const matchingIndices = (await getMatchingIndices({
- pattern,
- http,
- })) as string[];
- const matchingIndexPatterns = indexPatternsParam.filter((anIndexPattern) => {
- return anIndexPattern.includes(pattern);
- }) as string[];
+ const [matchingIndices, matchingIndexPatterns] = await Promise.all([
+ getMatchingIndices({
+ pattern,
+ http,
+ }),
+ loadIndexPatterns(pattern),
+ ]);
if (matchingIndices.length || matchingIndexPatterns.length) {
- const matchingOptions = uniq([...matchingIndices, ...matchingIndexPatterns]);
+ const matchingOptions = uniq([...(matchingIndices as string[]), ...matchingIndexPatterns]);
options.push({
label: i18n.translate(
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts
new file mode 100644
index 0000000000000..92908dbe4c4c7
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 {
+ loadIndexPatterns,
+ setSavedObjectsClient,
+ getMatchingIndices,
+ getESIndexFields,
+} from './data_apis';
+import { httpServiceMock } from 'src/core/public/mocks';
+
+const mockFind = jest.fn();
+const perPage = 1000;
+const http = httpServiceMock.createStartContract();
+const pattern = 'test-pattern';
+const indexes = ['test-index'];
+
+const generateIndexPattern = (title: string) => ({
+ attributes: {
+ title,
+ },
+});
+
+const mockIndices = { indices: ['indices1', 'indices2'] };
+const mockFields = {
+ fields: [
+ { name: 'name', type: 'type', normalizedType: 'nType', searchable: true, aggregatable: false },
+ ],
+};
+
+const mockPattern = 'test-pattern';
+
+describe('Data API', () => {
+ describe('index fields', () => {
+ test('fetches index fields', async () => {
+ http.post.mockResolvedValueOnce(mockFields);
+ const fields = await getESIndexFields({ indexes, http });
+
+ expect(http.post).toHaveBeenCalledWith('/api/triggers_actions_ui/data/_fields', {
+ body: `{"indexPatterns":${JSON.stringify(indexes)}}`,
+ });
+ expect(fields).toEqual(mockFields.fields);
+ });
+ });
+
+ describe('matching indices', () => {
+ test('fetches indices', async () => {
+ http.post.mockResolvedValueOnce(mockIndices);
+ const indices = await getMatchingIndices({ pattern, http });
+
+ expect(http.post).toHaveBeenCalledWith('/api/triggers_actions_ui/data/_indices', {
+ body: `{"pattern":"*${mockPattern}*"}`,
+ });
+ expect(indices).toEqual(mockIndices.indices);
+ });
+
+ test('returns empty array if fetch fails', async () => {
+ http.post.mockRejectedValueOnce(500);
+ const indices = await getMatchingIndices({ pattern, http });
+ expect(indices).toEqual([]);
+ });
+ });
+
+ describe('index patterns', () => {
+ beforeEach(() => {
+ setSavedObjectsClient({
+ find: mockFind,
+ });
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('fetches the index patterns', async () => {
+ mockFind.mockResolvedValueOnce({
+ savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')],
+ total: 2,
+ });
+ const results = await loadIndexPatterns(mockPattern);
+
+ expect(mockFind).toBeCalledTimes(1);
+ expect(mockFind).toBeCalledWith({
+ fields: ['title'],
+ page: 1,
+ perPage,
+ search: '*test-pattern*',
+ type: 'index-pattern',
+ });
+ expect(results).toEqual(['index-1', 'index-2']);
+ });
+
+ test(`fetches the index patterns as chunks and merges them, if the total number of index patterns more than ${perPage}`, async () => {
+ mockFind.mockResolvedValueOnce({
+ savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')],
+ total: 2010,
+ });
+ mockFind.mockResolvedValueOnce({
+ savedObjects: [generateIndexPattern('index-3'), generateIndexPattern('index-4')],
+ total: 2010,
+ });
+ mockFind.mockResolvedValueOnce({
+ savedObjects: [generateIndexPattern('index-5'), generateIndexPattern('index-6')],
+ total: 2010,
+ });
+ const results = await loadIndexPatterns(mockPattern);
+
+ expect(mockFind).toBeCalledTimes(3);
+ expect(mockFind).toHaveBeenNthCalledWith(1, {
+ fields: ['title'],
+ page: 1,
+ perPage,
+ search: '*test-pattern*',
+ type: 'index-pattern',
+ });
+ expect(mockFind).toHaveBeenNthCalledWith(2, {
+ fields: ['title'],
+ page: 2,
+ perPage,
+ search: '*test-pattern*',
+ type: 'index-pattern',
+ });
+ expect(mockFind).toHaveBeenNthCalledWith(3, {
+ fields: ['title'],
+ page: 3,
+ perPage,
+ search: '*test-pattern*',
+ type: 'index-pattern',
+ });
+ expect(results).toEqual(['index-1', 'index-2', 'index-3', 'index-4', 'index-5', 'index-6']);
+ });
+
+ test('returns an empty array if one of the requests fails', async () => {
+ mockFind.mockResolvedValueOnce({
+ savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')],
+ total: 1010,
+ });
+ mockFind.mockRejectedValueOnce(500);
+
+ const results = await loadIndexPatterns(mockPattern);
+
+ expect(results).toEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts
index d8a1ecabcd500..7ccf3bf71bec7 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts
@@ -9,6 +9,17 @@ import { HttpSetup } from 'kibana/public';
const DATA_API_ROOT = '/api/triggers_actions_ui/data';
+const formatPattern = (pattern: string) => {
+ let formattedPattern = pattern;
+ if (!formattedPattern.startsWith('*')) {
+ formattedPattern = `*${formattedPattern}`;
+ }
+ if (!formattedPattern.endsWith('*')) {
+ formattedPattern = `${formattedPattern}*`;
+ }
+ return formattedPattern;
+};
+
export async function getMatchingIndices({
pattern,
http,
@@ -16,17 +27,17 @@ export async function getMatchingIndices({
pattern: string;
http: HttpSetup;
}): Promise> {
- if (!pattern.startsWith('*')) {
- pattern = `*${pattern}`;
- }
- if (!pattern.endsWith('*')) {
- pattern = `${pattern}*`;
+ try {
+ const formattedPattern = formatPattern(pattern);
+
+ const { indices } = await http.post>(
+ `${DATA_API_ROOT}/_indices`,
+ { body: JSON.stringify({ pattern: formattedPattern }) }
+ );
+ return indices;
+ } catch (e) {
+ return [];
}
- const { indices } = await http.post>(
- `${DATA_API_ROOT}/_indices`,
- { body: JSON.stringify({ pattern }) }
- );
- return indices;
}
export async function getESIndexFields({
@@ -61,11 +72,48 @@ export const getSavedObjectsClient = () => {
return savedObjectsClient;
};
-export const loadIndexPatterns = async () => {
- const { savedObjects } = await getSavedObjectsClient().find({
- type: 'index-pattern',
- fields: ['title'],
- perPage: 10000,
- });
- return savedObjects;
+export const loadIndexPatterns = async (pattern: string) => {
+ let allSavedObjects = [];
+ const formattedPattern = formatPattern(pattern);
+ const perPage = 1000;
+
+ try {
+ const { savedObjects, total } = await getSavedObjectsClient().find({
+ type: 'index-pattern',
+ fields: ['title'],
+ page: 1,
+ search: formattedPattern,
+ perPage,
+ });
+
+ allSavedObjects = savedObjects;
+
+ if (total > perPage) {
+ let currentPage = 2;
+ const numberOfPages = Math.ceil(total / perPage);
+ const promises = [];
+
+ while (currentPage <= numberOfPages) {
+ promises.push(
+ getSavedObjectsClient().find({
+ type: 'index-pattern',
+ page: currentPage,
+ fields: ['title'],
+ search: formattedPattern,
+ perPage,
+ })
+ );
+ currentPage++;
+ }
+
+ const paginatedResults = await Promise.all(promises);
+
+ allSavedObjects = paginatedResults.reduce((oldResult, result) => {
+ return oldResult.concat(result.savedObjects);
+ }, allSavedObjects);
+ }
+ return allSavedObjects.map((indexPattern: any) => indexPattern.attributes.title);
+ } catch (e) {
+ return [];
+ }
};
From f8586a87d00082620436f18c5d258c1dfd7a2711 Mon Sep 17 00:00:00 2001
From: Rashmi Kulkarni
Date: Fri, 4 Mar 2022 14:02:09 -0800
Subject: [PATCH 16/27] fix for a skipped test `_scripted_fields_filter`
(#126866)
* fix for a skipped test
* cleanup
---
test/functional/apps/management/_scripted_fields_filter.js | 4 ++--
test/functional/page_objects/settings_page.ts | 5 ++++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js
index 117b8747c5a0a..abae9a300994d 100644
--- a/test/functional/apps/management/_scripted_fields_filter.js
+++ b/test/functional/apps/management/_scripted_fields_filter.js
@@ -16,8 +16,7 @@ export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['settings']);
- // FLAKY: https://github.com/elastic/kibana/issues/126027
- describe.skip('filter scripted fields', function describeIndexTests() {
+ describe('filter scripted fields', function describeIndexTests() {
before(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await browser.setWindowSize(1200, 800);
@@ -67,6 +66,7 @@ export default function ({ getService, getPageObjects }) {
expect(lang).to.be('painless');
}
});
+
await PageObjects.settings.clearScriptedFieldLanguageFilter('painless');
await PageObjects.settings.setScriptedFieldLanguageFilter('expression');
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index b1e4aa823821b..70cdbea7fa897 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -288,7 +288,10 @@ export class SettingsPageObject extends FtrService {
}
async setScriptedFieldLanguageFilter(language: string) {
- await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown');
+ await this.retry.try(async () => {
+ await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown');
+ return await this.find.byCssSelector('div.euiPopover__panel-isOpen');
+ });
await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover');
await this.testSubjects.existOrFail(`scriptedFieldLanguageFilterDropdown-option-${language}`);
await this.testSubjects.click(`scriptedFieldLanguageFilterDropdown-option-${language}`);
From 7af9c37016a373093c58aec5844ee040fcbcae72 Mon Sep 17 00:00:00 2001
From: Yara Tercero
Date: Fri, 4 Mar 2022 15:33:14 -0800
Subject: [PATCH 17/27] [Security Solution][Lists] - Hide exception list delete
icon if Kibana read only (#126710)
Addresses bug #126313
Even if user is given index privileges to lists, UI should follow Kibana privileges. Checks if user is a read only Kibana user and hides the delete icon from exception list view if true.
---
.../exceptions/exceptions_table.spec.ts | 21 ++++++++++
.../rules/all/exceptions/columns.tsx | 5 ++-
.../all/exceptions/exceptions_table.test.tsx | 42 ++++++++++++++-----
.../rules/all/exceptions/exceptions_table.tsx | 14 +++++--
4 files changed, 67 insertions(+), 15 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts
index 69bdafd5dccdd..d2578f9172033 100644
--- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { ROLES } from '../../../common/test';
import { getExceptionList, expectedExportedExceptionList } from '../../objects/exception';
import { getNewRule } from '../../objects/rule';
@@ -25,6 +26,7 @@ import {
clearSearchSelection,
} from '../../tasks/exceptions_table';
import {
+ EXCEPTIONS_TABLE_DELETE_BTN,
EXCEPTIONS_TABLE_LIST_NAME,
EXCEPTIONS_TABLE_SHOWING_LISTS,
} from '../../screens/exceptions';
@@ -168,3 +170,22 @@ describe('Exceptions Table', () => {
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1');
});
});
+
+describe('Exceptions Table - read only', () => {
+ before(() => {
+ // First we login as a privileged user to create exception list
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.platform_engineer);
+ createExceptionList(getExceptionList(), getExceptionList().list_id);
+
+ // Then we login as read-only user to test.
+ loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.reader);
+ waitForExceptionsTableToBeLoaded();
+
+ cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`);
+ });
+
+ it('Delete icon is not shown', () => {
+ cy.get(EXCEPTIONS_TABLE_DELETE_BTN).should('not.exist');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
index 78feb911ee082..33dff406734c9 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
@@ -27,7 +27,8 @@ export const getAllExceptionListsColumns = (
onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
formatUrl: FormatUrl,
- navigateToUrl: (url: string) => Promise
+ navigateToUrl: (url: string) => Promise,
+ isKibanaReadOnly: boolean
): AllExceptionListsColumns[] => [
{
align: 'left',
@@ -155,7 +156,7 @@ export const getAllExceptionListsColumns = (
},
{
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => {
- return listId === 'endpoint_list' ? (
+ return listId === 'endpoint_list' || isKibanaReadOnly ? (
<>>
) : (
({
- useUserData: jest.fn().mockReturnValue([
- {
- loading: false,
- canUserCRUD: false,
- },
- ]),
-}));
-
describe('ExceptionListsTable', () => {
const exceptionList1 = getExceptionListSchemaMock();
const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' };
@@ -86,9 +79,17 @@ describe('ExceptionListsTable', () => {
endpoint_list: exceptionList1,
},
]);
+
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: false,
+ canUserREAD: false,
+ },
+ ]);
});
- it('does not render delete option disabled if list is "endpoint_list"', async () => {
+ it('does not render delete option if list is "endpoint_list"', async () => {
const wrapper = mount(
@@ -106,4 +107,25 @@ describe('ExceptionListsTable', () => {
wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
).toBeFalsy();
});
+
+ it('does not render delete option if user is read only', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: false,
+ canUserREAD: true,
+ },
+ ]);
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(1).text()).toEqual(
+ 'not_endpoint_list'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button')).toHaveLength(0);
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index 4a7c71a1084a7..c40b6b9571724 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -60,7 +60,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
export const ExceptionListsTable = React.memo(() => {
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
- const [{ loading: userInfoLoading, canUserCRUD }] = useUserData();
+ const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
const hasPermissions = userHasPermissions(canUserCRUD);
const { loading: listsConfigLoading } = useListsConfig();
@@ -193,8 +193,16 @@ export const ExceptionListsTable = React.memo(() => {
);
const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => {
- return getAllExceptionListsColumns(handleExport, handleDelete, formatUrl, navigateToUrl);
- }, [handleExport, handleDelete, formatUrl, navigateToUrl]);
+ // Defaulting to true to default to the lower privilege first
+ const isKibanaReadOnly = (canUserREAD && !canUserCRUD) ?? true;
+ return getAllExceptionListsColumns(
+ handleExport,
+ handleDelete,
+ formatUrl,
+ navigateToUrl,
+ isKibanaReadOnly
+ );
+ }, [handleExport, handleDelete, formatUrl, navigateToUrl, canUserREAD, canUserCRUD]);
const handleRefresh = useCallback((): void => {
if (refreshExceptions != null) {
From 23f7cff88a28fbff83aa466ba38351a649db48cd Mon Sep 17 00:00:00 2001
From: Or Ouziel
Date: Sun, 6 Mar 2022 20:18:09 +0200
Subject: [PATCH 18/27] fix SO client bulkUpdate return type (#126349)
---
...kibana-plugin-core-public.savedobjectsclient.bulkupdate.md | 4 ++--
src/core/public/public.api.md | 2 +-
src/core/public/saved_objects/saved_objects_client.ts | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md
index 0e3bfb2bd896b..0cbfe4fcdead6 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md
@@ -9,7 +9,7 @@ Update multiple documents at once
Signature:
```typescript
-bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>;
+bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>;
```
## Parameters
@@ -20,7 +20,7 @@ bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): PromiseReturns:
-Promise<SavedObjectsBatchResponse<unknown>>
+Promise<SavedObjectsBatchResponse<T>>
The result of the update operation containing both failed and updated saved objects.
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index e3f2822b5a7c8..b30c009bf2538 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -1128,7 +1128,7 @@ export class SavedObjectsClient {
}>) => Promise<{
resolved_objects: ResolvedSimpleSavedObject[];
}>;
- bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>;
+ bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>;
create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>;
// Warning: (ae-forgotten-export) The symbol "SavedObjectsDeleteOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "SavedObjectsClientContract" needs to be exported by the entry point index.d.ts
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index c19233809a94b..8509ace047691 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -596,7 +596,7 @@ export class SavedObjectsClient {
return renameKeys<
PromiseType>,
SavedObjectsBatchResponse
- >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse;
+ >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse;
});
}
From 7ac836116313e458d793d8440f9cba7617ae4d14 Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Mon, 7 Mar 2022 01:33:26 -0700
Subject: [PATCH 19/27] [Metrics UI] Update position of legend and it's
controls (#115854)
* [Metrics UI] Update position of legend and it's controls
* updating button colors and moving history button back to the left
* updating legend placement
* removing unused dependencies
* Adding data-test-subj for legendControls
* removing unused deps
* Fix linting errors
* Move high value to top of legend
* Reclaim top space left open by GroupNameContainer
* Revert "Reclaim top space left open by GroupNameContainer"
This reverts commit 411e89e01d99432714b042d0c2b0fcb248874ee2.
This extra space is also serving as between-group margin. Also it doesn't solve the scrollbar overlap for multi-group cases.
* Move legend after waffle map in dom
This allows the waffle map to scroll without it overlapping the legend.
* Move show/hide to right
* Move timeline legend next to title
* Move "hide history" button into timeline area
* Revert "Move "hide history" button into timeline area"
This reverts commit e6725c106faccdef505f1ffda4827c2fa8036111.
* Revert "Move timeline legend next to title"
This reverts commit 3d204d3e566d87da3e43c7e2ca9411490a560ced.
* Revert "Move show/hide to right"
This reverts commit fd1b9bd6571322d1560828d92f8644124b27729a.
* Inline LegendControls and ViewSwitcher on mobile
* Better legend alignment with action buttons
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kate Farrar
Co-authored-by: Kate Farrar
Co-authored-by: Mat Schaffer
---
.../saved_views/toolbar_control.tsx | 1 +
.../components/bottom_drawer.tsx | 17 +-
.../inventory_view/components/layout.tsx | 323 +++++++++--------
.../components/nodes_overview.tsx | 7 +
.../components/waffle/legend.tsx | 46 +--
.../components/waffle/legend_controls.tsx | 340 +++++++++---------
.../waffle/stepped_gradient_legend.tsx | 92 ++---
.../components/waffle/view_switcher.tsx | 4 +-
8 files changed, 398 insertions(+), 432 deletions(-)
diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx
index b44f3ffa20df7..7b7c256d5ad59 100644
--- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx
+++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx
@@ -150,6 +150,7 @@ export function SavedViewsToolbarControls(props: Props) {
data-test-subj="savedViews-openPopover"
iconType="arrowDown"
iconSide="right"
+ color="text"
>
{currentView
? currentView.name
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
index 3681d740d93d0..ad548a632573f 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx
@@ -7,7 +7,7 @@
import React, { useCallback, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { useUiTracker } from '../../../../../../observability/public';
import { useWaffleOptionsContext } from '../hooks/use_waffle_options';
@@ -57,17 +57,6 @@ export const BottomDrawer: React.FC<{
{isOpen ? hideHistory : showHistory}
-
- {children}
-
-
@@ -97,7 +86,3 @@ const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({
const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })`
width: 140px;
`;
-
-const RightSideSpacer = euiStyled(EuiSpacer).attrs({ size: 'xs' })`
- width: 140px;
-`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
index 5a3dafaabbd17..7f3de57b610a4 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx
@@ -17,8 +17,12 @@ import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes';
import { PageContent } from '../../../../components/page';
import { useWaffleTimeContext } from '../hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../hooks/use_waffle_filters';
-import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options';
-import { InfraFormatterType } from '../../../../lib/lib';
+import {
+ DEFAULT_LEGEND,
+ useWaffleOptionsContext,
+ WaffleLegendOptions,
+} from '../hooks/use_waffle_options';
+import { InfraFormatterType, InfraWaffleMapBounds } from '../../../../lib/lib';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { Toolbar } from './toolbars/toolbar';
import { ViewSwitcher } from './waffle/view_switcher';
@@ -26,7 +30,7 @@ import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_f
import { createLegend } from '../lib/create_legend';
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
import { BottomDrawer } from './bottom_drawer';
-import { Legend } from './waffle/legend';
+import { LegendControls } from './waffle/legend_controls';
interface Props {
shouldLoadDefault: boolean;
@@ -37,149 +41,184 @@ interface Props {
loading: boolean;
}
-export const Layout = ({
- shouldLoadDefault,
- currentView,
- reload,
- interval,
- nodes,
- loading,
-}: Props) => {
- const [showLoading, setShowLoading] = useState(true);
- const { metric, groupBy, sort, nodeType, changeView, view, autoBounds, boundsOverride, legend } =
- useWaffleOptionsContext();
- const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
- const { applyFilterQuery } = useWaffleFiltersContext();
- const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette;
- const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps;
- const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors;
-
- const options = {
- formatter: InfraFormatterType.percent,
- formatTemplate: '{{value}}',
- legend: createLegend(legendPalette, legendSteps, legendReverseColors),
- metric,
- sort,
- groupBy,
- };
-
- useInterval(
- () => {
- if (!loading) {
- jumpToTime(Date.now());
- }
- },
- isAutoReloading ? 5000 : null
- );
-
- const dataBounds = calculateBoundsFromNodes(nodes);
- const bounds = autoBounds ? dataBounds : boundsOverride;
- /* eslint-disable-next-line react-hooks/exhaustive-deps */
- const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
- const { onViewChange } = useWaffleViewState();
-
- useEffect(() => {
- if (currentView) {
- onViewChange(currentView);
- }
- }, [currentView, onViewChange]);
-
- useEffect(() => {
- // load snapshot data after default view loaded, unless we're not loading a view
- if (currentView != null || !shouldLoadDefault) {
- reload();
- }
-
- /**
- * INFO: why disable exhaustive-deps
- * We need to wait on the currentView not to be null because it is loaded async and could change the view state.
- * We don't actually need to watch the value of currentView though, since the view state will be synched up by the
- * changing params in the reload method so we should only "watch" the reload method.
- *
- * TODO: Should refactor this in the future to make it more clear where all the view state is coming
- * from and it's precedence [query params, localStorage, defaultView, out of the box view]
- */
+interface LegendControlOptions {
+ auto: boolean;
+ bounds: InfraWaffleMapBounds;
+ legend: WaffleLegendOptions;
+}
+
+export const Layout = React.memo(
+ ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => {
+ const [showLoading, setShowLoading] = useState(true);
+ const {
+ metric,
+ groupBy,
+ sort,
+ nodeType,
+ changeView,
+ view,
+ autoBounds,
+ boundsOverride,
+ legend,
+ changeBoundsOverride,
+ changeAutoBounds,
+ changeLegend,
+ } = useWaffleOptionsContext();
+ const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
+ const { applyFilterQuery } = useWaffleFiltersContext();
+ const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette;
+ const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps;
+ const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors;
+
+ const options = {
+ formatter: InfraFormatterType.percent,
+ formatTemplate: '{{value}}',
+ legend: createLegend(legendPalette, legendSteps, legendReverseColors),
+ metric,
+ sort,
+ groupBy,
+ };
+
+ useInterval(
+ () => {
+ if (!loading) {
+ jumpToTime(Date.now());
+ }
+ },
+ isAutoReloading ? 5000 : null
+ );
+
+ const dataBounds = calculateBoundsFromNodes(nodes);
+ const bounds = autoBounds ? dataBounds : boundsOverride;
/* eslint-disable-next-line react-hooks/exhaustive-deps */
- }, [reload, shouldLoadDefault]);
-
- useEffect(() => {
- setShowLoading(true);
- }, [options.metric, nodeType]);
-
- useEffect(() => {
- const hasNodes = nodes && nodes.length;
- // Don't show loading screen when we're auto-reloading
- setShowLoading(!hasNodes);
- }, [nodes]);
-
- return (
- <>
-
-
- {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => (
-
-
- {({ measureRef: topActionMeasureRef, bounds: { height: topActionHeight = 0 } }) => (
- <>
-
-
-
-
-
-
-
-
-
- {({ measureRef, bounds: { height = 0 } }) => (
- <>
-
- {view === 'map' && (
- {
+ if (currentView) {
+ onViewChange(currentView);
+ }
+ }, [currentView, onViewChange]);
+
+ useEffect(() => {
+ // load snapshot data after default view loaded, unless we're not loading a view
+ if (currentView != null || !shouldLoadDefault) {
+ reload();
+ }
+
+ /**
+ * INFO: why disable exhaustive-deps
+ * We need to wait on the currentView not to be null because it is loaded async and could change the view state.
+ * We don't actually need to watch the value of currentView though, since the view state will be synched up by the
+ * changing params in the reload method so we should only "watch" the reload method.
+ *
+ * TODO: Should refactor this in the future to make it more clear where all the view state is coming
+ * from and it's precedence [query params, localStorage, defaultView, out of the box view]
+ */
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ }, [reload, shouldLoadDefault]);
+
+ useEffect(() => {
+ setShowLoading(true);
+ }, [options.metric, nodeType]);
+
+ useEffect(() => {
+ const hasNodes = nodes && nodes.length;
+ // Don't show loading screen when we're auto-reloading
+ setShowLoading(!hasNodes);
+ }, [nodes]);
+
+ const handleLegendControlChange = useCallback(
+ (opts: LegendControlOptions) => {
+ changeBoundsOverride(opts.bounds);
+ changeAutoBounds(opts.auto);
+ changeLegend(opts.legend);
+ },
+ [changeBoundsOverride, changeAutoBounds, changeLegend]
+ );
+
+ return (
+ <>
+
+
+ {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => (
+
+
+ {({
+ measureRef: topActionMeasureRef,
+ bounds: { height: topActionHeight = 0 },
+ }) => (
+ <>
+
+
+
+
+ {view === 'map' && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {({ measureRef, bounds: { height = 0 } }) => (
+ <>
+
-
+ {view === 'map' && (
+
-
- )}
- >
- )}
-
- >
- )}
-
-
- )}
-
-
- >
- );
-};
+ )}
+ >
+ )}
+
+ >
+ )}
+
+
+ )}
+
+
+ >
+ );
+ }
+);
const MainContainer = euiStyled.div`
position: relative;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
index 297f24e95bc4f..cec595e4be3d6 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx
@@ -18,6 +18,7 @@ import { Map } from './waffle/map';
import { TableView } from './table_view';
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes';
+import { Legend } from './waffle/legend';
export interface KueryFilterQuery {
kind: 'kuery';
@@ -131,6 +132,12 @@ export const NodesOverview = ({
bottomMargin={bottomMargin}
staticHeight={isStatic}
/>
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
index d305203b738c3..853aa98bf6244 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback } from 'react';
+import React from 'react';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import {
@@ -17,13 +17,7 @@ import {
GradientLegendRT,
} from '../../../../../lib/lib';
import { GradientLegend } from './gradient_legend';
-import { LegendControls } from './legend_controls';
import { StepLegend } from './steps_legend';
-import {
- DEFAULT_LEGEND,
- useWaffleOptionsContext,
- WaffleLegendOptions,
-} from '../../hooks/use_waffle_options';
import { SteppedGradientLegend } from './stepped_gradient_legend';
interface Props {
legend: InfraWaffleMapLegend;
@@ -32,39 +26,9 @@ interface Props {
formatter: InfraFormatter;
}
-interface LegendControlOptions {
- auto: boolean;
- bounds: InfraWaffleMapBounds;
- legend: WaffleLegendOptions;
-}
-
-export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }) => {
- const {
- changeBoundsOverride,
- changeAutoBounds,
- autoBounds,
- legend: legendOptions,
- changeLegend,
- boundsOverride,
- } = useWaffleOptionsContext();
- const handleChange = useCallback(
- (options: LegendControlOptions) => {
- changeBoundsOverride(options.bounds);
- changeAutoBounds(options.auto);
- changeLegend(options.legend);
- },
- [changeBoundsOverride, changeAutoBounds, changeLegend]
- );
+export const Legend: React.FC = ({ legend, bounds, formatter }) => {
return (
-
{GradientLegendRT.is(legend) && (
)}
@@ -77,8 +41,6 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter
};
const LegendContainer = euiStyled.div`
- position: absolute;
- bottom: 0px;
- left: 10px;
- right: 10px;
+ margin: 0 10px;
+ display: flex;
`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
index c7479434424a6..61b293888b85d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx
@@ -26,7 +26,6 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react';
import { first, last } from 'lodash';
-import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib';
import { WaffleLegendOptions } from '../../hooks/use_waffle_options';
import { getColorPalette } from '../../lib/get_color_palette';
@@ -78,8 +77,10 @@ export const LegendControls = ({
const buttonComponent = (
-
- Legend Options
-
-
- <>
-
-
-
- >
-
-
-
+ Legend Options
+
+
+ <>
+
-
-
+
+ >
+
+
+
-
+
+
+
+
+
+
+
+
+ }
+ isInvalid={!boundsValidRange}
+ display="columnCompressed"
+ error={errors}
+ >
+
+
-
-
-
+
+
+ }
+ isInvalid={!boundsValidRange}
+ error={errors}
+ >
+
+
-
-
+
+
+
+
+
+
- }
- isInvalid={!boundsValidRange}
- display="columnCompressed"
- error={errors}
- >
-
-
+
+
+
+
-
-
-
- }
- isInvalid={!boundsValidRange}
- error={errors}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
};
-
-const ControlContainer = euiStyled.div`
- position: absolute;
- top: -20px;
- right: 6px;
- bottom: 0;
-`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx
index a9bcfa7995c20..339426b126b9e 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import { EuiText } from '@elastic/eui';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import {
InfraWaffleMapBounds,
@@ -22,18 +23,19 @@ type TickValue = 0 | 1;
export const SteppedGradientLegend: React.FC
= ({ legend, bounds, formatter }) => {
return (
-
-
-
-
+
- {legend.rules.map((rule, index) => (
-
- ))}
+ {legend.rules
+ .slice()
+ .reverse()
+ .map((rule, index) => (
+
+ ))}
+
);
};
@@ -46,62 +48,38 @@ interface TickProps {
const TickLabel = ({ value, bounds, formatter }: TickProps) => {
const normalizedValue = value === 0 ? bounds.min : bounds.max * value;
- const style = { left: `${value * 100}%` };
const label = formatter(normalizedValue);
- return {label};
+ return (
+
+ {label}
+
+ );
};
-const GradientStep = euiStyled.div`
- height: ${(props) => props.theme.eui.paddingSizes.s};
- flex: 1 1 auto;
- &:first-child {
- border-radius: ${(props) => props.theme.eui.euiBorderRadius} 0 0 ${(props) =>
- props.theme.eui.euiBorderRadius};
- }
- &:last-child {
- border-radius: 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) =>
- props.theme.eui.euiBorderRadius} 0;
- }
+const LegendContainer = euiStyled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
`;
-const Ticks = euiStyled.div`
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- top: -18px;
+const GradientContainer = euiStyled.div`
+ height: 200px;
+ width: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
`;
-const Tick = euiStyled.div`
- position: absolute;
- font-size: 11px;
- text-align: center;
- top: 0;
- left: 0;
- white-space: nowrap;
- transform: translate(-50%, 0);
+const GradientStep = euiStyled.div`
+ flex: 1 1 auto;
&:first-child {
- padding-left: 5px;
- transform: translate(0, 0);
+ border-radius: ${(props) => props.theme.eui.euiBorderRadius} ${(props) =>
+ props.theme.eui.euiBorderRadius} 0 0;
}
&:last-child {
- padding-right: 5px;
- transform: translate(-100%, 0);
+ border-radius: 0 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) =>
+ props.theme.eui.euiBorderRadius};
}
`;
-
-const GradientContainer = euiStyled.div`
- display: flex;
- flex-direction; row;
- align-items: stretch;
- flex-grow: 1;
-`;
-
-const LegendContainer = euiStyled.div`
- position: absolute;
- height: 10px;
- bottom: 0;
- left: 0;
- right: 40px;
-`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
index 4dc288caa9833..8e911f7f82917 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx
@@ -37,8 +37,8 @@ export const ViewSwitcher = ({ view, onChange }: Props) => {
defaultMessage: 'Switch between table and map view',
})}
options={buttons}
- color="primary"
- buttonSize="m"
+ color="text"
+ buttonSize="s"
idSelected={view}
onChange={onChange}
isIconOnly
From 7c6d314cb0e91cde28db1c24b0acff21e447d839 Mon Sep 17 00:00:00 2001
From: Mark Hopkin
Date: Mon, 7 Mar 2022 10:47:18 +0000
Subject: [PATCH 20/27] [Fleet] Retry Saved Object import on conflict error
(#126900)
* retry SO import on conflict errors
* add jitter + increase retries
* Apply suggestions from code review
Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
---
.../epm/kibana/assets/install.test.ts | 124 ++++++++++++++++++
.../services/epm/kibana/assets/install.ts | 51 +++++--
2 files changed, 167 insertions(+), 8 deletions(-)
create mode 100644 x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts
new file mode 100644
index 0000000000000..51aee45c83cf3
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.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 {
+ ISavedObjectsImporter,
+ SavedObjectsImportFailure,
+ SavedObjectsImportSuccess,
+ SavedObjectsImportResponse,
+} from 'src/core/server';
+
+import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks';
+
+import type { ArchiveAsset } from './install';
+
+jest.mock('timers/promises', () => ({
+ async setTimeout() {},
+}));
+
+import { installKibanaSavedObjects } from './install';
+
+const mockLogger = loggingSystemMock.createLogger();
+
+const mockImporter: jest.Mocked = {
+ import: jest.fn(),
+ resolveImportErrors: jest.fn(),
+};
+
+const createImportError = (so: ArchiveAsset, type: string) =>
+ ({ id: so.id, error: { type } } as SavedObjectsImportFailure);
+const createImportSuccess = (so: ArchiveAsset) =>
+ ({ id: so.id, type: so.type, meta: {} } as SavedObjectsImportSuccess);
+const createAsset = (asset: Partial) =>
+ ({ id: 1234, type: 'dashboard', attributes: {}, ...asset } as ArchiveAsset);
+
+const createImportResponse = (
+ errors: SavedObjectsImportFailure[] = [],
+ successResults: SavedObjectsImportSuccess[] = []
+) =>
+ ({
+ success: !!successResults.length,
+ errors,
+ successResults,
+ warnings: [],
+ successCount: successResults.length,
+ } as SavedObjectsImportResponse);
+
+describe('installKibanaSavedObjects', () => {
+ beforeEach(() => {
+ mockImporter.import.mockReset();
+ mockImporter.resolveImportErrors.mockReset();
+ });
+
+ it('should retry on conflict error', async () => {
+ const asset = createAsset({ attributes: { hello: 'world' } });
+ const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]);
+ const successResponse = createImportResponse([], [createImportSuccess(asset)]);
+
+ mockImporter.import
+ .mockResolvedValueOnce(conflictResponse)
+ .mockResolvedValueOnce(successResponse);
+
+ await installKibanaSavedObjects({
+ savedObjectsImporter: mockImporter,
+ logger: mockLogger,
+ kibanaAssets: [asset],
+ });
+
+ expect(mockImporter.import).toHaveBeenCalledTimes(2);
+ });
+
+ it('should give up after 50 retries on conflict errors', async () => {
+ const asset = createAsset({ attributes: { hello: 'world' } });
+ const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]);
+
+ mockImporter.import.mockImplementation(() => Promise.resolve(conflictResponse));
+
+ await expect(
+ installKibanaSavedObjects({
+ savedObjectsImporter: mockImporter,
+ logger: mockLogger,
+ kibanaAssets: [asset],
+ })
+ ).rejects.toEqual(expect.any(Error));
+ expect(mockImporter.import).toHaveBeenCalledTimes(51);
+ });
+ it('should not retry errors that arent conflict errors', async () => {
+ const asset = createAsset({ attributes: { hello: 'world' } });
+ const errorResponse = createImportResponse([createImportError(asset, 'something_bad')]);
+ const successResponse = createImportResponse([], [createImportSuccess(asset)]);
+
+ mockImporter.import.mockResolvedValueOnce(errorResponse).mockResolvedValueOnce(successResponse);
+
+ expect(
+ installKibanaSavedObjects({
+ savedObjectsImporter: mockImporter,
+ logger: mockLogger,
+ kibanaAssets: [asset],
+ })
+ ).rejects.toEqual(expect.any(Error));
+ });
+
+ it('should resolve reference errors', async () => {
+ const asset = createAsset({ attributes: { hello: 'world' } });
+ const referenceErrorResponse = createImportResponse([
+ createImportError(asset, 'missing_references'),
+ ]);
+ const successResponse = createImportResponse([], [createImportSuccess(asset)]);
+
+ mockImporter.import.mockResolvedValueOnce(referenceErrorResponse);
+ mockImporter.resolveImportErrors.mockResolvedValueOnce(successResponse);
+
+ await installKibanaSavedObjects({
+ savedObjectsImporter: mockImporter,
+ logger: mockLogger,
+ kibanaAssets: [asset],
+ });
+
+ expect(mockImporter.import).toHaveBeenCalledTimes(1);
+ expect(mockImporter.resolveImportErrors).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
index 5ab15a1f52e75..d654fab427f19 100644
--- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { setTimeout } from 'timers/promises';
+
import type {
SavedObject,
SavedObjectsBulkCreateObject,
@@ -13,7 +15,6 @@ import type {
Logger,
} from 'src/core/server';
import type { SavedObjectsImportSuccess, SavedObjectsImportFailure } from 'src/core/server/types';
-
import { createListStream } from '@kbn/utils';
import { partition } from 'lodash';
@@ -166,7 +167,40 @@ export async function getKibanaAssets(
return result;
}
-async function installKibanaSavedObjects({
+const isImportConflictError = (e: SavedObjectsImportFailure) => e?.error?.type === 'conflict';
+/**
+ * retry saved object import if only conflict errors are encountered
+ */
+async function retryImportOnConflictError(
+ importCall: () => ReturnType,
+ {
+ logger,
+ maxAttempts = 50,
+ _attempt = 0,
+ }: { logger?: Logger; _attempt?: number; maxAttempts?: number } = {}
+): ReturnType {
+ const result = await importCall();
+
+ const errors = result.errors ?? [];
+ if (_attempt < maxAttempts && errors.length && errors.every(isImportConflictError)) {
+ const retryCount = _attempt + 1;
+ const retryDelayMs = 1000 + Math.floor(Math.random() * 3000); // 1s + 0-3s of jitter
+
+ logger?.debug(
+ `Retrying import operation after [${
+ retryDelayMs * 1000
+ }s] due to conflict errors: ${JSON.stringify(errors)}`
+ );
+
+ await setTimeout(retryDelayMs);
+ return retryImportOnConflictError(importCall, { logger, _attempt: retryCount });
+ }
+
+ return result;
+}
+
+// only exported for testing
+export async function installKibanaSavedObjects({
savedObjectsImporter,
kibanaAssets,
logger,
@@ -185,18 +219,19 @@ async function installKibanaSavedObjects({
return [];
} else {
const { successResults: importSuccessResults = [], errors: importErrors = [] } =
- await savedObjectsImporter.import({
- overwrite: true,
- readStream: createListStream(toBeSavedObjects),
- createNewCopies: false,
- });
+ await retryImportOnConflictError(() =>
+ savedObjectsImporter.import({
+ overwrite: true,
+ readStream: createListStream(toBeSavedObjects),
+ createNewCopies: false,
+ })
+ );
allSuccessResults = importSuccessResults;
const [referenceErrors, otherErrors] = partition(
importErrors,
(e) => e?.error?.type === 'missing_references'
);
-
if (otherErrors?.length) {
throw new Error(
`Encountered ${
From 3c9014737a9791bd9365b1fd0f880a5b5d8bdacf Mon Sep 17 00:00:00 2001
From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com>
Date: Mon, 7 Mar 2022 16:14:31 +0500
Subject: [PATCH 21/27] [Console] Support auto-complete for data streams
(#126235)
* Support auto-complete for data streams
* Add a test case
Co-authored-by: Muhammad Ibragimov
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../application/components/settings_modal.tsx | 11 +++++++++
.../data_stream_autocomplete_component.js | 20 ++++++++++++++++
.../lib/autocomplete/components/index.js | 1 +
src/plugins/console/public/lib/kb/kb.js | 4 ++++
.../public/lib/mappings/mapping.test.js | 9 +++++++
.../console/public/lib/mappings/mappings.js | 24 ++++++++++++++++---
.../console/public/services/settings.ts | 3 ++-
.../generated/indices.delete_data_stream.json | 2 +-
.../generated/indices.get_data_stream.json | 3 ++-
9 files changed, 71 insertions(+), 6 deletions(-)
create mode 100644 src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js
diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx
index c4be329dabcb8..eafc2dea3f873 100644
--- a/src/plugins/console/public/application/components/settings_modal.tsx
+++ b/src/plugins/console/public/application/components/settings_modal.tsx
@@ -70,6 +70,7 @@ export function DevToolsSettingsModal(props: Props) {
const [fields, setFields] = useState(props.settings.autocomplete.fields);
const [indices, setIndices] = useState(props.settings.autocomplete.indices);
const [templates, setTemplates] = useState(props.settings.autocomplete.templates);
+ const [dataStreams, setDataStreams] = useState(props.settings.autocomplete.dataStreams);
const [polling, setPolling] = useState(props.settings.polling);
const [pollInterval, setPollInterval] = useState(props.settings.pollInterval);
const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes);
@@ -97,12 +98,20 @@ export function DevToolsSettingsModal(props: Props) {
}),
stateSetter: setTemplates,
},
+ {
+ id: 'dataStreams',
+ label: i18n.translate('console.settingsPage.dataStreamsLabelText', {
+ defaultMessage: 'Data streams',
+ }),
+ stateSetter: setDataStreams,
+ },
];
const checkboxIdToSelectedMap = {
fields,
indices,
templates,
+ dataStreams,
};
const onAutocompleteChange = (optionId: AutocompleteOptions) => {
@@ -120,6 +129,7 @@ export function DevToolsSettingsModal(props: Props) {
fields,
indices,
templates,
+ dataStreams,
},
polling,
pollInterval,
@@ -170,6 +180,7 @@ export function DevToolsSettingsModal(props: Props) {
fields,
indices,
templates,
+ dataStreams,
});
}}
>
diff --git a/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js
new file mode 100644
index 0000000000000..015136b7670f5
--- /dev/null
+++ b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js
@@ -0,0 +1,20 @@
+/*
+ * 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 { getDataStreams } from '../../mappings/mappings';
+import { ListComponent } from './list_component';
+
+export class DataStreamAutocompleteComponent extends ListComponent {
+ constructor(name, parent, multiValued) {
+ super(name, getDataStreams, parent, multiValued);
+ }
+
+ getContextKey() {
+ return 'data_stream';
+ }
+}
diff --git a/src/plugins/console/public/lib/autocomplete/components/index.js b/src/plugins/console/public/lib/autocomplete/components/index.js
index 32078ee2c1519..4a8838a6fb821 100644
--- a/src/plugins/console/public/lib/autocomplete/components/index.js
+++ b/src/plugins/console/public/lib/autocomplete/components/index.js
@@ -23,4 +23,5 @@ export { IdAutocompleteComponent } from './id_autocomplete_component';
export { UsernameAutocompleteComponent } from './username_autocomplete_component';
export { IndexTemplateAutocompleteComponent } from './index_template_autocomplete_component';
export { ComponentTemplateAutocompleteComponent } from './component_template_autocomplete_component';
+export { DataStreamAutocompleteComponent } from './data_stream_autocomplete_component';
export * from './legacy';
diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js
index 5f02365a48fdf..e268f55be558e 100644
--- a/src/plugins/console/public/lib/kb/kb.js
+++ b/src/plugins/console/public/lib/kb/kb.js
@@ -16,6 +16,7 @@ import {
UsernameAutocompleteComponent,
IndexTemplateAutocompleteComponent,
ComponentTemplateAutocompleteComponent,
+ DataStreamAutocompleteComponent,
} from '../autocomplete/components';
import $ from 'jquery';
@@ -94,6 +95,9 @@ const parametrizedComponentFactories = {
component_template: function (name, parent) {
return new ComponentTemplateAutocompleteComponent(name, parent);
},
+ data_stream: function (name, parent) {
+ return new DataStreamAutocompleteComponent(name, parent);
+ },
};
export function getUnmatchedEndpointComponents() {
diff --git a/src/plugins/console/public/lib/mappings/mapping.test.js b/src/plugins/console/public/lib/mappings/mapping.test.js
index 9191eb736be3c..e2def74e892cc 100644
--- a/src/plugins/console/public/lib/mappings/mapping.test.js
+++ b/src/plugins/console/public/lib/mappings/mapping.test.js
@@ -266,4 +266,13 @@ describe('Mappings', () => {
expect(mappings.getIndexTemplates()).toEqual(expectedResult);
expect(mappings.getComponentTemplates()).toEqual(expectedResult);
});
+
+ test('Data streams', function () {
+ mappings.loadDataStreams({
+ data_streams: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }],
+ });
+
+ const expectedResult = ['test_index1', 'test_index2', 'test_index3'];
+ expect(mappings.getDataStreams()).toEqual(expectedResult);
+ });
});
diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js
index 75b8a263e8690..96a5665e730a2 100644
--- a/src/plugins/console/public/lib/mappings/mappings.js
+++ b/src/plugins/console/public/lib/mappings/mappings.js
@@ -17,6 +17,7 @@ let perAliasIndexes = [];
let legacyTemplates = [];
let indexTemplates = [];
let componentTemplates = [];
+let dataStreams = [];
const mappingObj = {};
@@ -60,6 +61,10 @@ export function getComponentTemplates() {
return [...componentTemplates];
}
+export function getDataStreams() {
+ return [...dataStreams];
+}
+
export function getFields(indices, types) {
// get fields for indices and types. Both can be a list, a string or null (meaning all).
let ret = [];
@@ -128,7 +133,9 @@ export function getTypes(indices) {
export function getIndices(includeAliases) {
const ret = [];
$.each(perIndexTypes, function (index) {
- ret.push(index);
+ if (!index.startsWith('.ds')) {
+ ret.push(index);
+ }
});
if (typeof includeAliases === 'undefined' ? true : includeAliases) {
$.each(perAliasIndexes, function (alias) {
@@ -204,6 +211,10 @@ export function loadComponentTemplates(data) {
componentTemplates = (data.component_templates ?? []).map(({ name }) => name);
}
+export function loadDataStreams(data) {
+ dataStreams = (data.data_streams ?? []).map(({ name }) => name);
+}
+
export function loadMappings(mappings) {
perIndexTypes = {};
@@ -265,6 +276,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) {
legacyTemplates: '_template',
indexTemplates: '_index_template',
componentTemplates: '_component_template',
+ dataStreams: '_data_stream',
};
// Fetch autocomplete info if setting is set to true, and if user has made changes.
@@ -326,14 +338,16 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) {
'componentTemplates',
templatesSettingToRetrieve
);
+ const dataStreamsPromise = retrieveSettings('dataStreams', settingsToRetrieve);
$.when(
mappingPromise,
aliasesPromise,
legacyTemplatesPromise,
indexTemplatesPromise,
- componentTemplatesPromise
- ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates) => {
+ componentTemplatesPromise,
+ dataStreamsPromise
+ ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates, dataStreams) => {
let mappingsResponse;
try {
if (mappings && mappings.length) {
@@ -365,6 +379,10 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) {
loadComponentTemplates(JSON.parse(componentTemplates[0]));
}
+ if (dataStreams) {
+ loadDataStreams(JSON.parse(dataStreams[0]));
+ }
+
if (mappings && aliases) {
// Trigger an update event with the mappings, aliases
$(mappingObj).trigger('update', [mappingsResponse, aliases[0]]);
diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts
index 058f6c20c1888..1a7eff3e7ca54 100644
--- a/src/plugins/console/public/services/settings.ts
+++ b/src/plugins/console/public/services/settings.ts
@@ -14,7 +14,7 @@ export const DEFAULT_SETTINGS = Object.freeze({
pollInterval: 60000,
tripleQuotes: true,
wrapMode: true,
- autocomplete: Object.freeze({ fields: true, indices: true, templates: true }),
+ autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }),
historyDisabled: false,
});
@@ -25,6 +25,7 @@ export interface DevToolsSettings {
fields: boolean;
indices: boolean;
templates: boolean;
+ dataStreams: boolean;
};
polling: boolean;
pollInterval: number;
diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json
index 9b91e3deb3a08..fb5cb446fb77e 100644
--- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json
+++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json
@@ -13,7 +13,7 @@
"DELETE"
],
"patterns": [
- "_data_stream/{name}"
+ "_data_stream/{data_stream}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html"
}
diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json
index 45199a60f337d..e383a1df4844a 100644
--- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json
+++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json
@@ -14,7 +14,8 @@
],
"patterns": [
"_data_stream",
- "_data_stream/{name}"
+ "_data_stream/{name}",
+ "{data_stream}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html"
}
From c9e66b8327d3d3087bcd1788aed121977515fac0 Mon Sep 17 00:00:00 2001
From: CohenIdo <90558359+CohenIdo@users.noreply.github.com>
Date: Mon, 7 Mar 2022 14:31:42 +0200
Subject: [PATCH 22/27] [Cloud Posture] add filterting for benchmark (#126980)
---
.../routes/benchmarks/benchmarks.test.ts | 30 +++++++++++++++++++
.../server/routes/benchmarks/benchmarks.ts | 15 ++++++++--
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts
index b728948cf2a05..8c9d04dc207f3 100644
--- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts
+++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts
@@ -76,6 +76,18 @@ describe('benchmarks API', () => {
});
});
+ it('expect to find benchmark_name', async () => {
+ const validatedQuery = benchmarksInputSchema.validate({
+ benchmark_name: 'my_cis_benchmark',
+ });
+
+ expect(validatedQuery).toMatchObject({
+ page: 1,
+ per_page: DEFAULT_BENCHMARKS_PER_PAGE,
+ benchmark_name: 'my_cis_benchmark',
+ });
+ });
+
it('should throw when page field is not a positive integer', async () => {
expect(() => {
benchmarksInputSchema.validate({ page: -2 });
@@ -125,6 +137,24 @@ describe('benchmarks API', () => {
});
});
+ it('should format request by benchmark_name', async () => {
+ const mockAgentPolicyService = createPackagePolicyServiceMock();
+
+ await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', {
+ page: 1,
+ per_page: 100,
+ benchmark_name: 'my_cis_benchmark',
+ });
+
+ expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject(
+ expect.objectContaining({
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *my_cis_benchmark*`,
+ page: 1,
+ perPage: 100,
+ })
+ );
+ });
+
describe('test getAgentPolicies', () => {
it('should return one agent policy id when there is duplication', async () => {
const agentPolicyService = createMockAgentPolicyService();
diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts
index 80c526c248c0f..c52aeead6cd4d 100644
--- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts
+++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts
@@ -43,8 +43,13 @@ export interface Benchmark {
export const DEFAULT_BENCHMARKS_PER_PAGE = 20;
export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies';
-const getPackageNameQuery = (packageName: string): string => {
- return `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`;
+const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => {
+ const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`;
+ const kquery = benchmarkFilter
+ ? `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*`
+ : integrationNameQuery;
+
+ return kquery;
};
export const getPackagePolicies = async (
@@ -57,7 +62,7 @@ export const getPackagePolicies = async (
throw new Error('packagePolicyService is undefined');
}
- const packageNameQuery = getPackageNameQuery(packageName);
+ const packageNameQuery = getPackageNameQuery(packageName, queryParams.benchmark_name);
const { items: packagePolicies } = (await packagePolicyService?.list(soClient, {
kuery: packageNameQuery,
@@ -193,4 +198,8 @@ export const benchmarksInputSchema = rt.object({
* The number of objects to include in each page
*/
per_page: rt.number({ defaultValue: DEFAULT_BENCHMARKS_PER_PAGE, min: 0 }),
+ /**
+ * Benchmark filter
+ */
+ benchmark_name: rt.maybe(rt.string()),
});
From bc0d9e70791be8dfdeec9770625950f884fb50e8 Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Mon, 7 Mar 2022 08:13:52 -0500
Subject: [PATCH 23/27] [Presentation] Fix some bugs with services dependency
injection (#126936)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../create/dependency_manager.test.ts | 54 +++++++++++++------
.../services/create/dependency_manager.ts | 50 +++++++++++------
2 files changed, 71 insertions(+), 33 deletions(-)
diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts
index 29702c3356865..8e67dee3f8b6b 100644
--- a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts
+++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts
@@ -24,6 +24,16 @@ describe('DependencyManager', () => {
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
});
+ it('should include final vertex if it has dependencies', () => {
+ const graph = {
+ A: [],
+ B: [],
+ C: ['A', 'B'],
+ };
+ const sortedTopology = ['A', 'B', 'C'];
+ expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
+ });
+
it('orderDependencies. Should return base topology if no depended vertices', () => {
const graph = {
N: [],
@@ -34,22 +44,34 @@ describe('DependencyManager', () => {
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
});
- it('orderDependencies. Should detect circular dependencies and throw error with path', () => {
- const graph = {
- N: ['R'],
- R: ['A'],
- A: ['B'],
- B: ['C'],
- C: ['D'],
- D: ['E'],
- E: ['F'],
- F: ['L'],
- L: ['G'],
- G: ['N'],
- };
- const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> ');
- const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`;
+ describe('circular dependencies', () => {
+ it('should detect circular dependencies and throw error with path', () => {
+ const graph = {
+ N: ['R'],
+ R: ['A'],
+ A: ['B'],
+ B: ['C'],
+ C: ['D'],
+ D: ['E'],
+ E: ['F'],
+ F: ['L'],
+ L: ['G'],
+ G: ['N'],
+ };
+ const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> ');
+ const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`;
+
+ expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage);
+ });
+
+ it('should detect circular dependency if circular reference is the first dependency for a vertex', () => {
+ const graph = {
+ A: ['B'],
+ B: ['A', 'C'],
+ C: [],
+ };
- expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage);
+ expect(() => DependencyManager.orderDependencies(graph)).toThrow();
+ });
});
});
diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts
index de30b180607fe..3925f3e9d9c4f 100644
--- a/src/plugins/presentation_util/public/services/create/dependency_manager.ts
+++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts
@@ -41,7 +41,14 @@ export class DependencyManager {
return cycleInfo;
}
- return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {});
+ return DependencyManager.sortVerticesFrom(
+ srcVertex,
+ graph,
+ sortedVertices,
+ {},
+ {},
+ cycleInfo
+ );
}, DependencyManager.createCycleInfo());
}
@@ -58,24 +65,30 @@ export class DependencyManager {
graph: Graph,
sortedVertices: Set,
visited: BreadCrumbs = {},
- inpath: BreadCrumbs = {}
+ inpath: BreadCrumbs = {},
+ cycle: CycleDetectionResult
): CycleDetectionResult {
visited[srcVertex] = true;
inpath[srcVertex] = true;
- const cycleInfo = graph[srcVertex]?.reduce | undefined>(
- (info, vertex) => {
- if (inpath[vertex]) {
- const path = (Object.keys(inpath) as T[]).filter(
- (visitedVertex) => inpath[visitedVertex]
- );
- return DependencyManager.createCycleInfo([...path, vertex], true);
- } else if (!visited[vertex]) {
- return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath);
- }
- return info;
- },
- undefined
- );
+
+ const vertexEdges =
+ graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex];
+
+ cycle = vertexEdges!.reduce>((info, vertex) => {
+ if (inpath[vertex]) {
+ return { ...info, hasCycle: true };
+ } else if (!visited[vertex]) {
+ return DependencyManager.sortVerticesFrom(
+ vertex,
+ graph,
+ sortedVertices,
+ visited,
+ inpath,
+ info
+ );
+ }
+ return info;
+ }, cycle);
inpath[srcVertex] = false;
@@ -83,7 +96,10 @@ export class DependencyManager {
sortedVertices.add(srcVertex);
}
- return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]);
+ return {
+ ...cycle,
+ path: [...sortedVertices],
+ };
}
private static createCycleInfo(
From f12891ee460536a9c7dbd1848290f0cb0d429502 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 7 Mar 2022 14:14:13 +0100
Subject: [PATCH 24/27] :bug: Handle case of undefined fitting for line/area
(#126891)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/vis_types/xy/public/config/get_config.ts | 4 ++--
.../options/point_series/elastic_charts_options.tsx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts
index d7cf22625e10e..7aad30c5b743e 100644
--- a/src/plugins/vis_types/xy/public/config/get_config.ts
+++ b/src/plugins/vis_types/xy/public/config/get_config.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { ScaleContinuousType } from '@elastic/charts';
+import { Fit, ScaleContinuousType } from '@elastic/charts';
import { Datatable } from '../../../../expressions/public';
import { BUCKET_TYPES } from '../../../../data/public';
@@ -92,7 +92,7 @@ export function getConfig(
return {
// NOTE: downscale ratio to match current vislib implementation
markSizeRatio: radiusRatio * 0.6,
- fittingFunction,
+ fittingFunction: fittingFunction ?? Fit.Linear,
fillOpacity,
detailedTooltip,
orderBucketsBySum,
diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx
index 105cd66799041..1c93fe92b79af 100644
--- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx
+++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx
@@ -78,7 +78,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps
})}
options={fittingFunctions}
paramName="fittingFunction"
- value={stateParams.fittingFunction}
+ value={stateParams.fittingFunction ?? fittingFunctions[2].value}
setValue={(paramName, value) => {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected');
From 0807b53d75f663db4fb8508e4aa729aaba2ad792 Mon Sep 17 00:00:00 2001
From: Matthew Kime
Date: Mon, 7 Mar 2022 07:23:35 -0600
Subject: [PATCH 25/27] fix data view load err msg (#126974)
---
src/plugins/data_views/common/data_views/data_views.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts
index 2e31ed793c3db..04c1fd98a0f60 100644
--- a/src/plugins/data_views/common/data_views/data_views.ts
+++ b/src/plugins/data_views/common/data_views/data_views.ts
@@ -424,7 +424,7 @@ export class DataViewsService {
);
if (!savedObject.version) {
- throw new SavedObjectNotFound(DATA_VIEW_SAVED_OBJECT_TYPE, id, 'management/kibana/dataViews');
+ throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews');
}
return this.initFromSavedObject(savedObject);
From 0adb328a9af39d35c02442198a74476ba86d9687 Mon Sep 17 00:00:00 2001
From: Matthew Kime
Date: Mon, 7 Mar 2022 07:24:14 -0600
Subject: [PATCH 26/27] [data views] Reenable data view validation functional
test (#125892)
* reenable test
---
...e_delete.js => _index_pattern_create_delete.ts} | 14 ++++++++------
test/functional/page_objects/settings_page.ts | 13 +++++++++++++
2 files changed, 21 insertions(+), 6 deletions(-)
rename test/functional/apps/management/{_index_pattern_create_delete.js => _index_pattern_create_delete.ts} (91%)
diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.ts
similarity index 91%
rename from test/functional/apps/management/_index_pattern_create_delete.js
rename to test/functional/apps/management/_index_pattern_create_delete.ts
index 4c9f5a5210ac6..6b2036499a1ed 100644
--- a/test/functional/apps/management/_index_pattern_create_delete.js
+++ b/test/functional/apps/management/_index_pattern_create_delete.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const browser = getService('browser');
@@ -35,8 +36,7 @@ export default function ({ getService, getPageObjects }) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/124663
- describe.skip('validation', function () {
+ describe('validation', function () {
it('can display errors', async function () {
await PageObjects.settings.clickAddNewIndexPatternButton();
await PageObjects.settings.setIndexPatternField('log-fake*');
@@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) {
it('can resolve errors and submit', async function () {
await PageObjects.settings.setIndexPatternField('log*');
- await (await PageObjects.settings.getSaveIndexPatternButton()).click();
+ await (await PageObjects.settings.getSaveDataViewButtonActive()).click();
await PageObjects.settings.removeIndexPattern();
});
});
@@ -72,10 +72,12 @@ export default function ({ getService, getPageObjects }) {
});
describe('index pattern creation', function indexPatternCreation() {
- let indexPatternId;
+ let indexPatternId: string;
before(function () {
- return PageObjects.settings.createIndexPattern().then((id) => (indexPatternId = id));
+ return PageObjects.settings
+ .createIndexPattern('logstash-*')
+ .then((id) => (indexPatternId = id));
});
it('should have index pattern in page header', async function () {
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 70cdbea7fa897..9c0fc73a23675 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -164,6 +164,19 @@ export class SettingsPageObject extends FtrService {
return await this.testSubjects.find('saveIndexPatternButton');
}
+ async getSaveDataViewButtonActive() {
+ await this.retry.try(async () => {
+ expect(
+ (
+ await this.find.allByCssSelector(
+ '[data-test-subj="saveIndexPatternButton"]:not(.euiButton-isDisabled)'
+ )
+ ).length
+ ).to.be(1);
+ });
+ return await this.testSubjects.find('saveIndexPatternButton');
+ }
+
async getCreateButton() {
return await this.find.displayedByCssSelector('[type="submit"]');
}
From 8b82657d46e18920e3a1acc3133f10f78e25a6f4 Mon Sep 17 00:00:00 2001
From: Matthew Kime
Date: Mon, 7 Mar 2022 07:24:39 -0600
Subject: [PATCH 27/27] [data views] functional tests to typescript (#126977)
js => ts
---
...ard.js => _create_index_pattern_wizard.ts} | 6 ++--
...x_pattern.js => _exclude_index_pattern.ts} | 3 +-
.../{_handle_alias.js => _handle_alias.ts} | 3 +-
...onflict.js => _handle_version_conflict.ts} | 4 +--
...ern_filter.js => _index_pattern_filter.ts} | 5 +--
...larity.js => _index_pattern_popularity.ts} | 5 +--
...sort.js => _index_pattern_results_sort.ts} | 10 +++---
...kibana_settings.js => _kibana_settings.ts} | 3 +-
...jects.js => _mgmt_import_saved_objects.ts} | 9 ++---
...{_runtime_fields.js => _runtime_fields.ts} | 9 +++--
...scripted_fields.js => _scripted_fields.ts} | 35 ++++++++++---------
...s_filter.js => _scripted_fields_filter.ts} | 3 +-
...preview.js => _scripted_fields_preview.ts} | 9 ++---
...st_huge_fields.js => _test_huge_fields.ts} | 5 +--
test/functional/page_objects/settings_page.ts | 6 ++--
15 files changed, 65 insertions(+), 50 deletions(-)
rename test/functional/apps/management/{_create_index_pattern_wizard.js => _create_index_pattern_wizard.ts} (93%)
rename test/functional/apps/management/{_exclude_index_pattern.js => _exclude_index_pattern.ts} (89%)
rename test/functional/apps/management/{_handle_alias.js => _handle_alias.ts} (95%)
rename test/functional/apps/management/{_handle_version_conflict.js => _handle_version_conflict.ts} (96%)
rename test/functional/apps/management/{_index_pattern_filter.js => _index_pattern_filter.ts} (90%)
rename test/functional/apps/management/{_index_pattern_popularity.js => _index_pattern_popularity.ts} (92%)
rename test/functional/apps/management/{_index_pattern_results_sort.js => _index_pattern_results_sort.ts} (90%)
rename test/functional/apps/management/{_kibana_settings.js => _kibana_settings.ts} (96%)
rename test/functional/apps/management/{_mgmt_import_saved_objects.js => _mgmt_import_saved_objects.ts} (80%)
rename test/functional/apps/management/{_runtime_fields.js => _runtime_fields.ts} (91%)
rename test/functional/apps/management/{_scripted_fields.js => _scripted_fields.ts} (96%)
rename test/functional/apps/management/{_scripted_fields_filter.js => _scripted_fields_filter.ts} (95%)
rename test/functional/apps/management/{_scripted_fields_preview.js => _scripted_fields_preview.ts} (90%)
rename test/functional/apps/management/{_test_huge_fields.js => _test_huge_fields.ts} (90%)
diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.ts
similarity index 93%
rename from test/functional/apps/management/_create_index_pattern_wizard.js
rename to test/functional/apps/management/_create_index_pattern_wizard.ts
index b2f24e530cb12..cf732e178aa74 100644
--- a/test/functional/apps/management/_create_index_pattern_wizard.js
+++ b/test/functional/apps/management/_create_index_pattern_wizard.ts
@@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
-export default function ({ getService, getPageObjects }) {
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const es = getService('es');
@@ -38,7 +40,7 @@ export default function ({ getService, getPageObjects }) {
body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] },
});
- await PageObjects.settings.createIndexPattern('alias1', false);
+ await PageObjects.settings.createIndexPattern('alias1', null);
});
it('can delete an index pattern', async () => {
diff --git a/test/functional/apps/management/_exclude_index_pattern.js b/test/functional/apps/management/_exclude_index_pattern.ts
similarity index 89%
rename from test/functional/apps/management/_exclude_index_pattern.js
rename to test/functional/apps/management/_exclude_index_pattern.ts
index b71222c1ec44d..8c20acdc21f92 100644
--- a/test/functional/apps/management/_exclude_index_pattern.js
+++ b/test/functional/apps/management/_exclude_index_pattern.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['settings']);
const es = getService('es');
diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.ts
similarity index 95%
rename from test/functional/apps/management/_handle_alias.js
rename to test/functional/apps/management/_handle_alias.ts
index 891e59d84a04b..04496bf9ed758 100644
--- a/test/functional/apps/management/_handle_alias.js
+++ b/test/functional/apps/management/_handle_alias.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const es = getService('es');
const retry = getService('retry');
diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.ts
similarity index 96%
rename from test/functional/apps/management/_handle_version_conflict.js
rename to test/functional/apps/management/_handle_version_conflict.ts
index a04c5d34b2d35..2f65f966c5596 100644
--- a/test/functional/apps/management/_handle_version_conflict.js
+++ b/test/functional/apps/management/_handle_version_conflict.ts
@@ -16,8 +16,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const browser = getService('browser');
@@ -93,7 +94,6 @@ export default function ({ getService, getPageObjects }) {
expect(response.body.result).to.be('updated');
await PageObjects.settings.controlChangeSave();
await retry.try(async function () {
- //await PageObjects.common.sleep(2000);
const message = await PageObjects.common.closeToast();
expect(message).to.contain('Unable');
});
diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.ts
similarity index 90%
rename from test/functional/apps/management/_index_pattern_filter.js
rename to test/functional/apps/management/_index_pattern_filter.ts
index 3e9d316b59c61..afa64c474d39d 100644
--- a/test/functional/apps/management/_index_pattern_filter.js
+++ b/test/functional/apps/management/_index_pattern_filter.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const PageObjects = getPageObjects(['settings']);
@@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) {
});
beforeEach(async function () {
- await PageObjects.settings.createIndexPattern();
+ await PageObjects.settings.createIndexPattern('logstash-*');
});
afterEach(async function () {
diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.ts
similarity index 92%
rename from test/functional/apps/management/_index_pattern_popularity.js
rename to test/functional/apps/management/_index_pattern_popularity.ts
index 1a71e4c5fbc68..bff6cdce0f7a6 100644
--- a/test/functional/apps/management/_index_pattern_popularity.js
+++ b/test/functional/apps/management/_index_pattern_popularity.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const log = getService('log');
@@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) {
});
beforeEach(async () => {
- await PageObjects.settings.createIndexPattern();
+ await PageObjects.settings.createIndexPattern('logstash-*');
// increase Popularity of geo.coordinates
log.debug('Starting openControlsByName (' + fieldName + ')');
await PageObjects.settings.openControlsByName(fieldName);
diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.ts
similarity index 90%
rename from test/functional/apps/management/_index_pattern_results_sort.js
rename to test/functional/apps/management/_index_pattern_results_sort.ts
index cedf5ee355b36..305a72889e95a 100644
--- a/test/functional/apps/management/_index_pattern_results_sort.js
+++ b/test/functional/apps/management/_index_pattern_results_sort.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const PageObjects = getPageObjects(['settings', 'common']);
@@ -18,7 +19,7 @@ export default function ({ getService, getPageObjects }) {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
- await PageObjects.settings.createIndexPattern();
+ await PageObjects.settings.createIndexPattern('logstash-*');
});
after(async function () {
@@ -30,7 +31,7 @@ export default function ({ getService, getPageObjects }) {
heading: 'Name',
first: '@message',
last: 'xss.raw',
- selector: async function () {
+ async selector() {
const tableRow = await PageObjects.settings.getTableRow(0, 0);
return await tableRow.getVisibleText();
},
@@ -39,7 +40,7 @@ export default function ({ getService, getPageObjects }) {
heading: 'Type',
first: '',
last: 'text',
- selector: async function () {
+ async selector() {
const tableRow = await PageObjects.settings.getTableRow(0, 1);
return await tableRow.getVisibleText();
},
@@ -49,7 +50,6 @@ export default function ({ getService, getPageObjects }) {
columns.forEach(function (col) {
describe('sort by heading - ' + col.heading, function indexPatternCreation() {
it('should sort ascending', async function () {
- console.log('col.heading', col.heading);
if (col.heading !== 'Name') {
await PageObjects.settings.sortBy(col.heading);
}
diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.ts
similarity index 96%
rename from test/functional/apps/management/_kibana_settings.js
rename to test/functional/apps/management/_kibana_settings.ts
index cfe4e88cda21d..d459643849fbc 100644
--- a/test/functional/apps/management/_kibana_settings.js
+++ b/test/functional/apps/management/_kibana_settings.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const browser = getService('browser');
const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']);
diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.ts
similarity index 80%
rename from test/functional/apps/management/_mgmt_import_saved_objects.js
rename to test/functional/apps/management/_mgmt_import_saved_objects.ts
index 95b0bbb7ed03b..04a1bb5938322 100644
--- a/test/functional/apps/management/_mgmt_import_saved_objects.js
+++ b/test/functional/apps/management/_mgmt_import_saved_objects.ts
@@ -8,13 +8,14 @@
import expect from '@kbn/expect';
import path from 'path';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
- //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization
- //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238)
+ // in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization
+ // that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238)
describe('mgmt saved objects', function describeIndexTests() {
before(async () => {
@@ -41,7 +42,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.savedObjects.waitTableIsLoaded();
await PageObjects.savedObjects.searchForObject('mysaved');
- //instead of asserting on count- am asserting on the titles- which is more accurate than count.
+ // instead of asserting on count- am asserting on the titles- which is more accurate than count.
const objects = await PageObjects.savedObjects.getRowTitles();
expect(objects.includes('mysavedsearch')).to.be(true);
expect(objects.includes('mysavedviz')).to.be(true);
diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.ts
similarity index 91%
rename from test/functional/apps/management/_runtime_fields.js
rename to test/functional/apps/management/_runtime_fields.ts
index 3a70df81b55d9..8ec9fb92c58ea 100644
--- a/test/functional/apps/management/_runtime_fields.js
+++ b/test/functional/apps/management/_runtime_fields.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const log = getService('log');
const browser = getService('browser');
@@ -36,7 +37,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10);
await log.debug('add runtime field');
await PageObjects.settings.addRuntimeField(
fieldName,
@@ -51,7 +52,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.clickSaveField();
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1);
+ expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be(
+ startingCount + 1
+ );
});
});
diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.ts
similarity index 96%
rename from test/functional/apps/management/_scripted_fields.js
rename to test/functional/apps/management/_scripted_fields.ts
index 72f45e1fedb4d..c8c605ec7ed19 100644
--- a/test/functional/apps/management/_scripted_fields.js
+++ b/test/functional/apps/management/_scripted_fields.ts
@@ -23,8 +23,9 @@
// it will automatically insert a a closing square brace ], etc.
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const log = getService('log');
const browser = getService('browser');
@@ -77,7 +78,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10);
await PageObjects.settings.clickScriptedFieldsTab();
await log.debug('add scripted field');
const script = `1`;
@@ -90,7 +91,7 @@ export default function ({ getService, getPageObjects }) {
script
);
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(
+ expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be(
startingCount + 1
);
});
@@ -111,7 +112,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10);
await PageObjects.settings.clickScriptedFieldsTab();
await log.debug('add scripted field');
const script = `if (doc['machine.ram'].size() == 0) return -1;
@@ -126,7 +127,7 @@ export default function ({ getService, getPageObjects }) {
script
);
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(
+ expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be(
startingCount + 1
);
});
@@ -150,7 +151,7 @@ export default function ({ getService, getPageObjects }) {
});
});
- //add a test to sort numeric scripted field
+ // add a test to sort numeric scripted field
it('should sort scripted field value in Discover', async function () {
await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`);
// after the first click on the scripted field, it becomes secondary sort after time.
@@ -201,7 +202,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10);
await PageObjects.settings.clickScriptedFieldsTab();
await log.debug('add scripted field');
await PageObjects.settings.addScriptedField(
@@ -213,7 +214,7 @@ export default function ({ getService, getPageObjects }) {
"if (doc['response.raw'].value == '200') { return 'good'} else { return 'bad'}"
);
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(
+ expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be(
startingCount + 1
);
});
@@ -237,7 +238,7 @@ export default function ({ getService, getPageObjects }) {
});
});
- //add a test to sort string scripted field
+ // add a test to sort string scripted field
it('should sort scripted field value in Discover', async function () {
await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`);
// after the first click on the scripted field, it becomes secondary sort after time.
@@ -287,7 +288,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10);
await PageObjects.settings.clickScriptedFieldsTab();
await log.debug('add scripted field');
await PageObjects.settings.addScriptedField(
@@ -299,7 +300,7 @@ export default function ({ getService, getPageObjects }) {
"doc['response.raw'].value == '200'"
);
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(
+ expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be(
startingCount + 1
);
});
@@ -335,8 +336,8 @@ export default function ({ getService, getPageObjects }) {
await filterBar.removeAllFilters();
});
- //add a test to sort boolean
- //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped.
+ // add a test to sort boolean
+ // existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped.
it.skip('should sort scripted field value in Discover', async function () {
await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`);
// after the first click on the scripted field, it becomes secondary sort after time.
@@ -374,7 +375,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
- const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount());
+ const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10);
await PageObjects.settings.clickScriptedFieldsTab();
await log.debug('add scripted field');
await PageObjects.settings.addScriptedField(
@@ -386,7 +387,7 @@ export default function ({ getService, getPageObjects }) {
"doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60"
);
await retry.try(async function () {
- expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(
+ expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be(
startingCount + 1
);
});
@@ -410,8 +411,8 @@ export default function ({ getService, getPageObjects }) {
});
});
- //add a test to sort date scripted field
- //https://github.com/elastic/kibana/issues/75711
+ // add a test to sort date scripted field
+ // https://github.com/elastic/kibana/issues/75711
it.skip('should sort scripted field value in Discover', async function () {
await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`);
// after the first click on the scripted field, it becomes secondary sort after time.
diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.ts
similarity index 95%
rename from test/functional/apps/management/_scripted_fields_filter.js
rename to test/functional/apps/management/_scripted_fields_filter.ts
index abae9a300994d..82d1590819750 100644
--- a/test/functional/apps/management/_scripted_fields_filter.js
+++ b/test/functional/apps/management/_scripted_fields_filter.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const log = getService('log');
diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.ts
similarity index 90%
rename from test/functional/apps/management/_scripted_fields_preview.js
rename to test/functional/apps/management/_scripted_fields_preview.ts
index b6c941fe21d0a..380b4659c0f38 100644
--- a/test/functional/apps/management/_scripted_fields_preview.js
+++ b/test/functional/apps/management/_scripted_fields_preview.ts
@@ -7,13 +7,14 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const PageObjects = getPageObjects(['settings']);
const SCRIPTED_FIELD_NAME = 'myScriptedField';
- const scriptResultToJson = (scriptResult) => {
+ const scriptResultToJson = (scriptResult: string) => {
try {
return JSON.parse(scriptResult);
} catch (e) {
@@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) {
await browser.setWindowSize(1200, 800);
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
- await PageObjects.settings.createIndexPattern();
+ await PageObjects.settings.createIndexPattern('logstash-*');
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
@@ -67,7 +68,7 @@ export default function ({ getService, getPageObjects }) {
it('should display additional fields', async function () {
const scriptResults = await PageObjects.settings.executeScriptedField(
`doc['bytes'].value * 2`,
- ['bytes']
+ 'bytes'
);
const [{ _id, bytes }] = scriptResultToJson(scriptResults);
expect(_id).to.be.a('string');
diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.ts
similarity index 90%
rename from test/functional/apps/management/_test_huge_fields.js
rename to test/functional/apps/management/_test_huge_fields.ts
index 7b75683940928..abc338cb8abc8 100644
--- a/test/functional/apps/management/_test_huge_fields.js
+++ b/test/functional/apps/management/_test_huge_fields.ts
@@ -7,8 +7,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function ({ getService, getPageObjects }) {
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const PageObjects = getPageObjects(['common', 'home', 'settings']);
@@ -19,7 +20,7 @@ export default function ({ getService, getPageObjects }) {
const EXPECTED_FIELD_COUNT = '10006';
before(async function () {
- await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false);
+ await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']);
await esArchiver.emptyKibanaIndex();
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields');
await PageObjects.settings.navigateTo();
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 9c0fc73a23675..98fdff82e13c5 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -563,7 +563,7 @@ export class SettingsPageObject extends FtrService {
name: string,
language: string,
type: string,
- format: Record,
+ format: Record | null,
popularity: string,
script: string
) {
@@ -803,7 +803,7 @@ export class SettingsPageObject extends FtrService {
await this.flyout.ensureClosed('scriptedFieldsHelpFlyout');
}
- async executeScriptedField(script: string, additionalField: string) {
+ async executeScriptedField(script: string, additionalField?: string) {
this.log.debug('execute Scripted Fields help');
await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked
await this.setScriptedFieldScript(script);
@@ -814,7 +814,7 @@ export class SettingsPageObject extends FtrService {
await this.testSubjects.click('runScriptButton');
await this.testSubjects.waitForDeleted('.euiLoadingSpinner');
}
- let scriptResults;
+ let scriptResults: string = '';
await this.retry.try(async () => {
scriptResults = await this.testSubjects.getVisibleText('scriptedFieldPreview');
});