From df913b47bee8ccf0e836c5866ef6b4345004813d Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Fri, 29 Jan 2021 14:06:14 -0700
Subject: [PATCH 01/18] Update build_chromium README (#89762)
* Update build_chromium README
* more edits
* Update init.py
---
x-pack/build_chromium/README.md | 59 +++++++++++++++++++++------------
x-pack/build_chromium/build.py | 4 +--
x-pack/build_chromium/init.py | 12 ++++---
3 files changed, 46 insertions(+), 29 deletions(-)
diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md
index 9934d06a9d96a..39382620775ad 100644
--- a/x-pack/build_chromium/README.md
+++ b/x-pack/build_chromium/README.md
@@ -6,50 +6,65 @@ to accept a commit hash from the Chromium repository, and initialize the build
environments and run the build on Mac, Windows, and Linux.
## Before you begin
+
If you wish to use a remote VM to build, you'll need access to our GCP account,
which is where we have two machines provisioned for the Linux and Windows
builds. Mac builds can be achieved locally, and are a great place to start to
gain familiarity.
+**NOTE:** Linux builds should be done in Ubuntu on x86 architecture. ARM builds
+are created in x86. CentOS is not supported for building Chromium.
+
1. Login to our GCP instance [here using your okta credentials](https://console.cloud.google.com/).
2. Click the "Compute Engine" tab.
-3. Ensure that `chromium-build-linux` and `chromium-build-windows-12-beefy` are there.
-4. If #3 fails, you'll have to spin up new instances. Generally, these need `n1-standard-8` types or 8 vCPUs/30 GB memory.
-5. Ensure that there's enough room left on the disk: 100GB is required. `ncdu` is a good linux util to verify what's claming space.
-
-## Usage
+3. Find `chromium-build-linux` or `chromium-build-windows-12-beefy` and start the instance.
+4. Install [Google Cloud SDK](https://cloud.google.com/sdk) locally to ssh into the GCP instance
+5. System dependencies:
+ - 8 CPU
+ - 30GB memory
+ - 80GB free space on disk (Try `ncdu /home` to see where space is used.)
+ - git
+ - python2 (`python` must link to `python2`)
+ - lsb_release
+ - tmux is recommended in case your ssh session is interrupted
+6. Copy the entire `build_chromium` directory into a GCP storage bucket, so you can copy the scripts into the instance and run them.
+
+## Build Script Usage
```
+# Allow our scripts to use depot_tools commands
export PATH=$HOME/chromium/depot_tools:$PATH
+
# Create a dedicated working directory for this directory of Python scripts.
mkdir ~/chromium && cd ~/chromium
+
# Copy the scripts from the Kibana repo to use them conveniently in the working directory
-cp -r ~/path/to/kibana/x-pack/build_chromium .
-# Install the OS packages, configure the environment, download the chromium source
+gsutil cp -r gs://my-bucket/build_chromium .
+
+# Install the OS packages, configure the environment, download the chromium source (25GB)
python ./build_chromium/init.sh [arch_name]
# Run the build script with the path to the chromium src directory, the git commit id
-python ./build_chromium/build.py
+python ./build_chromium/build.py x86
-# You can add an architecture flag for ARM
+# OR You can build for ARM
python ./build_chromium/build.py arm64
```
+**NOTE:** The `init.py` script updates git config to make it more possible for
+the Chromium repo to be cloned successfully. If checking out the Chromium fails
+with "early EOF" errors, the instance could be low on memory or disk space.
+
## Getting the Commit ID
-Getting `` can be tricky. The best technique seems to be:
+The `build.py` script requires a commit ID of the Chromium repo. Getting `` can be tricky. The best technique seems to be:
1. Create a temporary working directory and intialize yarn
2. `yarn add puppeteer # install latest puppeter`
-3. Look through puppeteer's node module files to find the "chromium revision" (a custom versioning convention for Chromium).
+3. Look through Puppeteer documentation and Changelogs to find information
+about where the "chromium revision" is located in the Puppeteer code. The code
+containing it might not be distributed in the node module.
+ - Example: https://github.com/puppeteer/puppeteer/blob/b549256/src/revisions.ts
4. Use `https://crrev.com` and look up the revision and find the git commit info.
-
-The official Chromium build process is poorly documented, and seems to have
-breaking changes fairly regularly. The build pre-requisites, and the build
-flags change over time, so it is likely that the scripts in this directory will
-be out of date by the time we have to do another Chromium build.
-
-This document is an attempt to note all of the gotchas we've come across while
-building, so that the next time we have to tinker here, we'll have a good
-starting point.
+ - Example: http://crrev.com/818858 leads to the git commit e62cb7e3fc7c40548cef66cdf19d270535d9350b
## Build args
@@ -115,8 +130,8 @@ The more cores the better, as the build makes effective use of each. For Linux,
- Linux:
- SSH in using [gcloud](https://cloud.google.com/sdk/)
- - Get the ssh command in the [GCP console](https://console.cloud.google.com/) -> VM instances -> your-vm-name -> SSH -> gcloud
- - Their in-browser UI is kinda sluggish, so use the commandline tool
+ - Get the ssh command in the [GCP console](https://console.cloud.google.com/) -> VM instances -> your-vm-name -> SSH -> "View gcloud command"
+ - Their in-browser UI is kinda sluggish, so use the commandline tool (Google Cloud SDK is required)
- Windows:
- Install Microsoft's Remote Desktop tools
diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py
index 8622f4a9d4c0b..0064f48ae973f 100644
--- a/x-pack/build_chromium/build.py
+++ b/x-pack/build_chromium/build.py
@@ -33,10 +33,10 @@
base_version = source_version[:7].strip('.')
# Set to "arm" to build for ARM on Linux
-arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'x64'
+arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'unknown'
if arch_name != 'x64' and arch_name != 'arm64':
- raise Exception('Unexpected architecture: ' + arch_name)
+ raise Exception('Unexpected architecture: ' + arch_name + '. `x64` and `arm64` are supported.')
print('Building Chromium ' + source_version + ' for ' + arch_name + ' from ' + src_path)
print('src path: ' + src_path)
diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py
index c0dd60f1cfcb0..3a2e28a884b09 100644
--- a/x-pack/build_chromium/init.py
+++ b/x-pack/build_chromium/init.py
@@ -8,18 +8,19 @@
# call this once the platform-specific initialization has completed.
# Set to "arm" to build for ARM on Linux
-arch_name = sys.argv[1] if len(sys.argv) >= 2 else 'x64'
+arch_name = sys.argv[1] if len(sys.argv) >= 2 else 'undefined'
build_path = path.abspath(os.curdir)
src_path = path.abspath(path.join(build_path, 'chromium', 'src'))
if arch_name != 'x64' and arch_name != 'arm64':
- raise Exception('Unexpected architecture: ' + arch_name)
+ raise Exception('Unexpected architecture: ' + arch_name + '. `x64` and `arm64` are supported.')
# Configure git
print('Configuring git globals...')
runcmd('git config --global core.autocrlf false')
runcmd('git config --global core.filemode false')
runcmd('git config --global branch.autosetuprebase always')
+runcmd('git config --global core.compression 0')
# Grab Chromium's custom build tools, if they aren't already installed
# (On Windows, they are installed before this Python script is run)
@@ -35,13 +36,14 @@
runcmd('git pull origin master')
os.chdir(original_dir)
-configure_environment(arch_name, build_path, src_path)
-
# Fetch the Chromium source code
chromium_dir = path.join(build_path, 'chromium')
if not path.isdir(chromium_dir):
mkdir(chromium_dir)
os.chdir(chromium_dir)
- runcmd('fetch chromium')
+ runcmd('fetch chromium --nohooks=1 --no-history=1')
else:
print('Directory exists: ' + chromium_dir + '. Skipping chromium fetch.')
+
+# This depends on having the chromium/src directory with the complete checkout
+configure_environment(arch_name, build_path, src_path)
From 3720006cf8a5c264390a59e60d9403e0a3e9906f Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Fri, 29 Jan 2021 17:05:27 -0500
Subject: [PATCH 02/18] [CI] Move Jest tests to separate machines (#89770)
---
vars/kibanaPipeline.groovy | 28 +++++++++++++++++++++-------
vars/tasks.groovy | 5 +----
vars/workers.groovy | 2 ++
3 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index 3e72c9e059af8..3032d88c26d98 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -447,13 +447,27 @@ def withTasks(Map params = [worker: [:]], Closure closure) {
}
def allCiTasks() {
- withTasks {
- tasks.check()
- tasks.lint()
- tasks.test()
- tasks.functionalOss()
- tasks.functionalXpack()
- }
+ parallel([
+ general: {
+ withTasks {
+ tasks.check()
+ tasks.lint()
+ tasks.test()
+ tasks.functionalOss()
+ tasks.functionalXpack()
+ }
+ },
+ jest: {
+ workers.ci(name: 'jest', size: 'c2-8', ramDisk: true) {
+ scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh')()
+ }
+ },
+ xpackJest: {
+ workers.ci(name: 'xpack-jest', size: 'c2-8', ramDisk: true) {
+ scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh')()
+ }
+ },
+ ])
}
def pipelineLibraryTests() {
diff --git a/vars/tasks.groovy b/vars/tasks.groovy
index 3493a95f0bdce..6c4f897691136 100644
--- a/vars/tasks.groovy
+++ b/vars/tasks.groovy
@@ -30,12 +30,9 @@ def lint() {
def test() {
tasks([
- // These 2 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here
+ // This task requires isolation because of hard-coded, conflicting ports and such, so let's use Docker here
kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'),
-
- kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'),
kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'),
- kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'),
])
}
diff --git a/vars/workers.groovy b/vars/workers.groovy
index dd634f3c25a32..e1684f7aadb43 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -19,6 +19,8 @@ def label(size) {
return 'docker && tests-xl-highmem'
case 'xxl':
return 'docker && tests-xxl && gobld/machineType:custom-64-270336'
+ case 'c2-8':
+ return 'docker && linux && immutable && gobld/machineType:c2-standard-8'
}
error "unknown size '${size}'"
From 2a913e4eb192b52bc12d3f66c1dd69f07205a08e Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Fri, 29 Jan 2021 15:53:29 -0700
Subject: [PATCH 03/18] Skips flake tests and tests with what looks like bugs
(#89777)
## Summary
Skips tests that have flake or in-determinism.
* The sourcer code/tests are being rewritten and then those will come back by other team members.
* The timeline open dialog looks to have some click and indeterminism bugs that are being investigated. Skipping for now.
---
.../cypress/integration/data_sources/sourcerer.spec.ts | 4 +++-
.../cypress/integration/timelines/creation.spec.ts | 5 +++--
x-pack/plugins/security_solution/cypress/tasks/timelines.ts | 5 ++++-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts
index 8b5871a6a67db..857582aac7638 100644
--- a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts
@@ -28,7 +28,9 @@ import { populateTimeline } from '../../tasks/timeline';
import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline';
import { cleanKibana } from '../../tasks/common';
-describe('Sourcerer', () => {
+// Skipped at the moment as this has flake due to click handler issues. This has been raised with team members
+// and the code is being re-worked and then these tests will be unskipped
+describe.skip('Sourcerer', () => {
before(() => {
cleanKibana();
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
index 2bfd2fbf0054c..ac70a1cae148e 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
@@ -47,7 +47,8 @@ import { openTimeline } from '../../tasks/timelines';
import { OVERVIEW_URL } from '../../urls/navigation';
-describe('Timelines', () => {
+// Skipped at the moment as there looks to be in-deterministic bugs with the open timeline dialog.
+describe.skip('Timelines', () => {
beforeEach(() => {
cleanKibana();
});
@@ -89,7 +90,7 @@ describe('Timelines', () => {
cy.get(FAVORITE_TIMELINE).should('exist');
cy.get(TIMELINE_TITLE).should('have.text', timeline.title);
- cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description);
+ cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); // This is the flake part where it sometimes does not show/load the timelines correctly
cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query} `);
cy.get(TIMELINE_FILTER(timeline.filter)).should('exist');
cy.get(PIN_EVENT)
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts
index a04ecb1f9ccaa..c2b5790b1ae12 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts
@@ -19,7 +19,10 @@ export const exportTimeline = (timelineId: string) => {
};
export const openTimeline = (id: string) => {
- cy.get(TIMELINE(id), { timeout: 500 }).click();
+ // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe.
+ // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/
+ // Ref: https://github.com/NicholasBoll/cypress-pipe#readme
+ cy.get(TIMELINE(id)).should('be.visible').wait(1500).click();
};
export const waitForTimelinesPanelToBeLoaded = () => {
From 2f80e44d3b2a1820b88b7b0c5a02922f768374ce Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Fri, 29 Jan 2021 19:16:19 -0700
Subject: [PATCH 04/18] [Security Solution][Detection Engine] Fixes indicator
matches mapping UI where invalid list values can cause overwrites of other
values (#89066)
## Summary
This fixes the ReactJS keys to not use array indexes for the ReactJS keys which fixes https://github.com/elastic/kibana/issues/84893 as well as a few other bugs that I will show below. The fix for the ReactJS keys is to add a unique id version 4 `uuid.v4()` to the incoming threat_mapping and the entities. On save out to elastic I remove the id. This is considered [better practices for ReactJS keys](https://reactjs.org/docs/lists-and-keys.html)
Down the road we might augment the arrays to have that id information but for now I add them when we get the data and then remove them as we save the data.
This PR also:
* Fixes tech debt around the hooks to remove the disabling of the `react-hooks/exhaustive-deps` in a few areas
* Fixes one React Hook misnamed that would not have triggered React linter rules (_useRuleAsyn)
* Adds 23 new Cypress e2e tests
* Adds a new pattern of dealing with on button clicks for the Cypress tests that are make it less flakey
```ts
cy.get(`button[title="${indexField}"]`)
.should('be.visible')
.then(([e]) => e.click());
```
* Adds several new utilities to Cypress for testing rows for indicator matches and other Cypress utils to improve velocity and ergonomics
```ts
fillIndicatorMatchRow
getDefineContinueButton
getIndicatorInvalidationText
getIndicatorIndexComboField
getIndicatorDeleteButton
getIndicatorOrButton
getIndicatorAndButton
```
## Bug 1
Deleting row 1 can cause row 2 to be cleared out or only partial data to stick around.
Before:
![im_bug_1](https://user-images.githubusercontent.com/1151048/105916137-c57b1d80-5fed-11eb-95b7-ad25b71cf4b8.gif)
After:
![im_fix_1_1](https://user-images.githubusercontent.com/1151048/105917509-9fef1380-5fef-11eb-98eb-025c226f79fe.gif)
## Bug 2
Deleting row 2 in the middle of 3 rows did not shift the value up correctly
Before:
![im_bug_2](https://user-images.githubusercontent.com/1151048/105917584-c01ed280-5fef-11eb-8c5b-fefb36f81008.gif)
After:
![im_fix_2](https://user-images.githubusercontent.com/1151048/105917650-e0e72800-5fef-11eb-9fd3-020d52e4e3b1.gif)
## Bug 3
When using OR with values it does not shift up correctly similar to AND
Before:
![im_bug_3](https://user-images.githubusercontent.com/1151048/105917691-f2303480-5fef-11eb-9368-b11d23159606.gif)
After:
![im_fix_3](https://user-images.githubusercontent.com/1151048/105917714-f9574280-5fef-11eb-9be4-1f56c207525a.gif)
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---
.../indicator_match_rule.spec.ts | 412 ++++++++++++++----
.../cypress/screens/create_new_rule.ts | 16 +
.../cypress/tasks/create_new_rule.ts | 152 ++++++-
.../threat_match/entry_item.test.tsx | 9 +-
.../components/threat_match/entry_item.tsx | 12 +-
.../components/threat_match/helpers.test.tsx | 15 +-
.../components/threat_match/helpers.tsx | 33 +-
.../common/components/threat_match/index.tsx | 76 ++--
.../threat_match/list_item.test.tsx | 9 -
.../components/threat_match/list_item.tsx | 4 +-
.../components/threat_match/reducer.test.ts | 8 +
.../common/components/threat_match/types.ts | 1 +
.../utils/add_remove_id_to_item.test.ts | 76 ++++
.../common/utils/add_remove_id_to_item.ts | 49 +++
.../alerts/use_privilege_user.tsx | 7 +-
.../detection_engine/alerts/use_query.tsx | 4 +-
.../alerts/use_signal_index.tsx | 3 +-
.../detection_engine/rules/transforms.ts | 98 +++++
.../rules/use_create_rule.tsx | 10 +-
.../rules/use_pre_packaged_rules.tsx | 10 +-
.../detection_engine/rules/use_rule.tsx | 18 +-
.../detection_engine/rules/use_rule_async.tsx | 12 +-
.../rules/use_rule_status.tsx | 6 +-
.../detection_engine/rules/use_tags.tsx | 7 +-
.../rules/use_update_rule.tsx | 11 +-
25 files changed, 857 insertions(+), 201 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts
create mode 100644 x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts
create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
index 37123dedfd661..2c9dc14aa05b2 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
@@ -5,7 +5,7 @@
*/
import { formatMitreAttackDescription } from '../../helpers/rules';
-import { newThreatIndicatorRule } from '../../objects/rule';
+import { indexPatterns, newThreatIndicatorRule } from '../../objects/rule';
import {
ALERT_RULE_METHOD,
@@ -70,7 +70,24 @@ import {
createAndActivateRule,
fillAboutRuleAndContinue,
fillDefineIndicatorMatchRuleAndContinue,
+ fillIndexAndIndicatorIndexPattern,
+ fillIndicatorMatchRow,
fillScheduleRuleAndContinue,
+ getCustomIndicatorQueryInput,
+ getCustomQueryInput,
+ getCustomQueryInvalidationText,
+ getDefineContinueButton,
+ getIndexPatternClearButton,
+ getIndexPatternInvalidationText,
+ getIndicatorAndButton,
+ getIndicatorAtLeastOneInvalidationText,
+ getIndicatorDeleteButton,
+ getIndicatorIndex,
+ getIndicatorIndexComboField,
+ getIndicatorIndicatorIndex,
+ getIndicatorInvalidationText,
+ getIndicatorMappingComboField,
+ getIndicatorOrButton,
selectIndicatorMatchType,
waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
@@ -92,14 +109,6 @@ describe('Detection rules, Indicator Match', () => {
cleanKibana();
esArchiverLoad('threat_indicator');
esArchiverLoad('threat_data');
- });
-
- afterEach(() => {
- esArchiverUnload('threat_indicator');
- esArchiverUnload('threat_data');
- });
-
- it('Creates and activates a new Indicator Match rule', () => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
@@ -107,89 +116,330 @@ describe('Detection rules, Indicator Match', () => {
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded();
goToCreateNewRule();
selectIndicatorMatchType();
- fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule);
- fillAboutRuleAndContinue(newThreatIndicatorRule);
- fillScheduleRuleAndContinue(newThreatIndicatorRule);
- createAndActivateRule();
+ });
+
+ afterEach(() => {
+ esArchiverUnload('threat_indicator');
+ esArchiverUnload('threat_data');
+ });
- cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
+ describe('Creating new indicator match rules', () => {
+ describe('Index patterns', () => {
+ it('Contains a predefined index pattern', () => {
+ getIndicatorIndex().should('have.text', indexPatterns.join(''));
+ });
- changeToThreeHundredRowsPerPage();
- waitForRulesToBeLoaded();
+ it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => {
+ getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`);
+ getDefineContinueButton().click();
+ getIndexPatternInvalidationText().should('not.exist');
+ });
- cy.get(RULES_TABLE).then(($table) => {
- cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
+ it('Shows invalidation text when you try to continue without filling it out', () => {
+ getIndexPatternClearButton().click();
+ getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`);
+ getDefineContinueButton().click();
+ getIndexPatternInvalidationText().should('exist');
+ });
});
- filterByCustomRules();
+ describe('Indicator index patterns', () => {
+ it('Contains empty index pattern', () => {
+ getIndicatorIndicatorIndex().should('have.text', '');
+ });
+
+ it('Does NOT show invalidation text on initial page load', () => {
+ getIndexPatternInvalidationText().should('not.exist');
+ });
- cy.get(RULES_TABLE).then(($table) => {
- cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
+ it('Shows invalidation text if you try to continue without filling it out', () => {
+ getDefineContinueButton().click();
+ getIndexPatternInvalidationText().should('exist');
+ });
});
- cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name);
- cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore);
- cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity);
- cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
-
- goToRuleDetails();
-
- cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`);
- cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description);
- cy.get(ABOUT_DETAILS).within(() => {
- getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity);
- getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore);
- getDetails(REFERENCE_URLS_DETAILS).should((details) => {
- expect(removeExternalLinkText(details.text())).equal(expectedUrls);
- });
- getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
- getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
- expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
- });
- getDetails(TAGS_DETAILS).should('have.text', expectedTags);
+
+ describe('custom query input', () => {
+ it('Has a default set of *:*', () => {
+ getCustomQueryInput().should('have.text', '*:*');
+ });
+
+ it('Shows invalidation text if text is removed', () => {
+ getCustomQueryInput().type('{selectall}{del}');
+ getCustomQueryInvalidationText().should('exist');
+ });
});
- cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
- cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
-
- cy.get(DEFINITION_DETAILS).within(() => {
- getDetails(INDEX_PATTERNS_DETAILS).should(
- 'have.text',
- newThreatIndicatorRule.index!.join('')
- );
- getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*');
- getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match');
- getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
- getDetails(INDICATOR_INDEX_PATTERNS).should(
- 'have.text',
- newThreatIndicatorRule.indicatorIndexPattern.join('')
- );
- getDetails(INDICATOR_MAPPING).should(
- 'have.text',
- `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}`
- );
- getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*');
+
+ describe('custom indicator query input', () => {
+ it('Has a default set of *:*', () => {
+ getCustomIndicatorQueryInput().should('have.text', '*:*');
+ });
+
+ it('Shows invalidation text if text is removed', () => {
+ getCustomIndicatorQueryInput().type('{selectall}{del}');
+ getCustomQueryInvalidationText().should('exist');
+ });
});
- cy.get(SCHEDULE_DETAILS).within(() => {
- getDetails(RUNS_EVERY_DETAILS).should(
- 'have.text',
- `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}`
- );
- getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
- 'have.text',
- `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}`
- );
+ describe('Indicator mapping', () => {
+ beforeEach(() => {
+ fillIndexAndIndicatorIndexPattern(
+ newThreatIndicatorRule.index,
+ newThreatIndicatorRule.indicatorIndexPattern
+ );
+ });
+
+ it('Does NOT show invalidation text on initial page load', () => {
+ getIndicatorInvalidationText().should('not.exist');
+ });
+
+ it('Shows invalidation text when you try to press continue without filling anything out', () => {
+ getDefineContinueButton().click();
+ getIndicatorAtLeastOneInvalidationText().should('exist');
+ });
+
+ it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => {
+ getIndicatorAndButton().click();
+ getIndicatorInvalidationText().should('exist');
+ });
+
+ it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => {
+ getIndicatorOrButton().click();
+ getIndicatorInvalidationText().should('exist');
+ });
+
+ it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getDefineContinueButton().click();
+ getIndicatorInvalidationText().should('not.exist');
+ });
+
+ it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => {
+ fillIndicatorMatchRow({
+ indexField: 'non-existent-value',
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ validColumns: 'indicatorField',
+ });
+ getDefineContinueButton().click();
+ getIndicatorInvalidationText().should('exist');
+ });
+
+ it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: 'non-existent-value',
+ validColumns: 'indexField',
+ });
+ getDefineContinueButton().click();
+ getIndicatorInvalidationText().should('exist');
+ });
+
+ it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getIndicatorAndButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 2,
+ indexField: 'agent.name',
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ validColumns: 'indicatorField',
+ });
+ getIndicatorDeleteButton().click();
+ getIndicatorIndexComboField().should('have.text', 'agent.name');
+ getIndicatorMappingComboField().should(
+ 'have.text',
+ newThreatIndicatorRule.indicatorIndexField
+ );
+ getIndicatorIndexComboField(2).should('not.exist');
+ getIndicatorMappingComboField(2).should('not.exist');
+ });
+
+ it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: 'non-existent-value',
+ validColumns: 'indexField',
+ });
+ getIndicatorAndButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 2,
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: 'second-non-existent-value',
+ validColumns: 'indexField',
+ });
+ getIndicatorDeleteButton().click();
+ getIndicatorMappingComboField().should('have.text', 'second-non-existent-value');
+ getIndicatorIndexComboField(2).should('not.exist');
+ getIndicatorMappingComboField(2).should('not.exist');
+ });
+
+ it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => {
+ fillIndicatorMatchRow({
+ indexField: 'non-existent-value',
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ validColumns: 'indicatorField',
+ });
+ getIndicatorAndButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 2,
+ indexField: 'second-non-existent-value',
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ validColumns: 'indicatorField',
+ });
+ getIndicatorDeleteButton().click();
+ getIndicatorIndexComboField().should('have.text', 'second-non-existent-value');
+ getIndicatorIndexComboField(2).should('not.exist');
+ getIndicatorMappingComboField(2).should('not.exist');
+ });
+
+ it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getIndicatorDeleteButton().click();
+ getIndicatorIndexComboField().should('text', 'Search');
+ getIndicatorMappingComboField().should('text', 'Search');
+ getIndicatorIndexComboField(2).should('not.exist');
+ getIndicatorMappingComboField(2).should('not.exist');
+ });
+
+ it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => {
+ fillIndicatorMatchRow({
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getIndicatorAndButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 2,
+ indexField: 'non-existent-value',
+ indicatorIndexField: 'non-existent-value',
+ validColumns: 'none',
+ });
+ getIndicatorAndButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 3,
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getIndicatorDeleteButton(2).click();
+ getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping);
+ getIndicatorMappingComboField(1).should('text', newThreatIndicatorRule.indicatorIndexField);
+ getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping);
+ getIndicatorMappingComboField(2).should('text', newThreatIndicatorRule.indicatorIndexField);
+ getIndicatorIndexComboField(3).should('not.exist');
+ getIndicatorMappingComboField(3).should('not.exist');
+ });
+
+ it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => {
+ fillIndicatorMatchRow({
+ indexField: 'non-existent-value-one',
+ indicatorIndexField: 'non-existent-value-two',
+ validColumns: 'none',
+ });
+ getIndicatorOrButton().click();
+ fillIndicatorMatchRow({
+ rowNumber: 2,
+ indexField: newThreatIndicatorRule.indicatorMapping,
+ indicatorIndexField: newThreatIndicatorRule.indicatorIndexField,
+ });
+ getIndicatorDeleteButton().click();
+ getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping);
+ getIndicatorMappingComboField().should('text', newThreatIndicatorRule.indicatorIndexField);
+ getIndicatorIndexComboField(2).should('not.exist');
+ getIndicatorMappingComboField(2).should('not.exist');
+ });
});
- waitForTheRuleToBeExecuted();
- waitForAlertsToPopulate();
-
- cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
- cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name);
- cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
- cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match');
- cy.get(ALERT_RULE_SEVERITY)
- .first()
- .should('have.text', newThreatIndicatorRule.severity.toLowerCase());
- cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore);
+ it('Creates and activates a new Indicator Match rule', () => {
+ fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule);
+ fillAboutRuleAndContinue(newThreatIndicatorRule);
+ fillScheduleRuleAndContinue(newThreatIndicatorRule);
+ createAndActivateRule();
+
+ cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
+
+ changeToThreeHundredRowsPerPage();
+ waitForRulesToBeLoaded();
+
+ cy.get(RULES_TABLE).then(($table) => {
+ cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
+ });
+
+ filterByCustomRules();
+
+ cy.get(RULES_TABLE).then(($table) => {
+ cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
+ });
+ cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name);
+ cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore);
+ cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity);
+ cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
+
+ goToRuleDetails();
+
+ cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`);
+ cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description);
+ cy.get(ABOUT_DETAILS).within(() => {
+ getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity);
+ getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore);
+ getDetails(REFERENCE_URLS_DETAILS).should((details) => {
+ expect(removeExternalLinkText(details.text())).equal(expectedUrls);
+ });
+ getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
+ getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
+ expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
+ });
+ getDetails(TAGS_DETAILS).should('have.text', expectedTags);
+ });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
+ cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
+
+ cy.get(DEFINITION_DETAILS).within(() => {
+ getDetails(INDEX_PATTERNS_DETAILS).should(
+ 'have.text',
+ newThreatIndicatorRule.index!.join('')
+ );
+ getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*');
+ getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match');
+ getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
+ getDetails(INDICATOR_INDEX_PATTERNS).should(
+ 'have.text',
+ newThreatIndicatorRule.indicatorIndexPattern.join('')
+ );
+ getDetails(INDICATOR_MAPPING).should(
+ 'have.text',
+ `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}`
+ );
+ getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*');
+ });
+
+ cy.get(SCHEDULE_DETAILS).within(() => {
+ getDetails(RUNS_EVERY_DETAILS).should(
+ 'have.text',
+ `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}`
+ );
+ getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
+ 'have.text',
+ `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}`
+ );
+ });
+
+ waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
+
+ cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
+ cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name);
+ cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
+ cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match');
+ cy.get(ALERT_RULE_SEVERITY)
+ .first()
+ .should('have.text', newThreatIndicatorRule.severity.toLowerCase());
+ cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
index 66681e77b7eb9..2a59dd33399c5 100644
--- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
@@ -38,6 +38,22 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
export const THREAT_MATCH_QUERY_INPUT =
'[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]';
+export const THREAT_MATCH_AND_BUTTON = '[data-test-subj="andButton"]';
+
+export const THREAT_ITEM_ENTRY_DELETE_BUTTON = '[data-test-subj="itemEntryDeleteButton"]';
+
+export const THREAT_MATCH_OR_BUTTON = '[data-test-subj="orButton"]';
+
+export const THREAT_COMBO_BOX_INPUT = '[data-test-subj="fieldAutocompleteComboBox"]';
+
+export const INVALID_MATCH_CONTENT = 'All matches require both a field and threat index field.';
+
+export const AT_LEAST_ONE_VALID_MATCH = 'At least one indicator match is required.';
+
+export const AT_LEAST_ONE_INDEX_PATTERN = 'A minimum of one index pattern is required.';
+
+export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.';
+
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
index 7836960b1a694..5143dc27e7d7a 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
@@ -63,13 +63,20 @@ import {
EQL_QUERY_PREVIEW_HISTOGRAM,
EQL_QUERY_VALIDATION_SPINNER,
COMBO_BOX_CLEAR_BTN,
- COMBO_BOX_RESULT,
MITRE_ATTACK_TACTIC_DROPDOWN,
MITRE_ATTACK_TECHNIQUE_DROPDOWN,
MITRE_ATTACK_SUBTECHNIQUE_DROPDOWN,
MITRE_ATTACK_ADD_TACTIC_BUTTON,
MITRE_ATTACK_ADD_SUBTECHNIQUE_BUTTON,
MITRE_ATTACK_ADD_TECHNIQUE_BUTTON,
+ THREAT_COMBO_BOX_INPUT,
+ THREAT_ITEM_ENTRY_DELETE_BUTTON,
+ THREAT_MATCH_AND_BUTTON,
+ INVALID_MATCH_CONTENT,
+ THREAT_MATCH_OR_BUTTON,
+ AT_LEAST_ONE_VALID_MATCH,
+ AT_LEAST_ONE_INDEX_PATTERN,
+ CUSTOM_QUERY_REQUIRED,
} from '../screens/create_new_rule';
import { TOAST_ERROR } from '../screens/shared';
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
@@ -144,7 +151,7 @@ export const fillAboutRuleAndContinue = (
rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule
) => {
fillAboutRule(rule);
- cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true });
+ getAboutContinueButton().should('exist').click({ force: true });
};
export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => {
@@ -222,7 +229,7 @@ export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => {
cy.get(COMBO_BOX_INPUT).type(`${rule.timestampOverride}{enter}`);
});
- cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true });
+ getAboutContinueButton().should('exist').click({ force: true });
};
export const fillDefineCustomRuleWithImportedQueryAndContinue = (
@@ -282,19 +289,132 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => {
cy.get(EQL_QUERY_INPUT).should('not.exist');
};
+/**
+ * Fills in the indicator match rows for tests by giving it an optional rowNumber,
+ * a indexField, a indicatorIndexField, and an optional validRows which indicates
+ * which row is valid or not.
+ *
+ * There are special tricks below with Eui combo box:
+ * cy.get(`button[title="${indexField}"]`)
+ * .should('be.visible')
+ * .then(([e]) => e.click());
+ *
+ * To first ensure the button is there before clicking on the button. There are
+ * race conditions where if the Eui drop down button from the combo box is not
+ * visible then the click handler is not there either, and when we click on it
+ * that will cause the item to _not_ be selected. Using a {enter} with the combo
+ * box also does not select things from EuiCombo boxes either, so I have to click
+ * the actual contents of the EuiCombo box to select things.
+ */
+export const fillIndicatorMatchRow = ({
+ rowNumber,
+ indexField,
+ indicatorIndexField,
+ validColumns,
+}: {
+ rowNumber?: number; // default is 1
+ indexField: string;
+ indicatorIndexField: string;
+ validColumns?: 'indexField' | 'indicatorField' | 'both' | 'none'; // default is both are valid entries
+}) => {
+ const computedRowNumber = rowNumber == null ? 1 : rowNumber;
+ const computedValueRows = validColumns == null ? 'both' : validColumns;
+ const OFFSET = 2;
+ cy.get(COMBO_BOX_INPUT)
+ .eq(computedRowNumber * OFFSET + 1)
+ .type(indexField);
+ if (computedValueRows === 'indexField' || computedValueRows === 'both') {
+ cy.get(`button[title="${indexField}"]`)
+ .should('be.visible')
+ .then(([e]) => e.click());
+ }
+ cy.get(COMBO_BOX_INPUT)
+ .eq(computedRowNumber * OFFSET + 2)
+ .type(indicatorIndexField);
+
+ if (computedValueRows === 'indicatorField' || computedValueRows === 'both') {
+ cy.get(`button[title="${indicatorIndexField}"]`)
+ .should('be.visible')
+ .then(([e]) => e.click());
+ }
+};
+
+/**
+ * Fills in both the index pattern and the indicator match index pattern.
+ * @param indexPattern The index pattern.
+ * @param indicatorIndex The indicator index pattern.
+ */
+export const fillIndexAndIndicatorIndexPattern = (
+ indexPattern?: string[],
+ indicatorIndex?: string[]
+) => {
+ getIndexPatternClearButton().click();
+ getIndicatorIndex().type(`${indexPattern}{enter}`);
+ getIndicatorIndicatorIndex().type(`${indicatorIndex}{enter}`);
+};
+
+/** Returns the indicator index drop down field. Pass in row number, default is 1 */
+export const getIndicatorIndexComboField = (row = 1) =>
+ cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 2);
+
+/** Returns the indicator mapping drop down field. Pass in row number, default is 1 */
+export const getIndicatorMappingComboField = (row = 1) =>
+ cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 1);
+
+/** Returns the indicator matches DELETE button for the mapping. Pass in row number, default is 1 */
+export const getIndicatorDeleteButton = (row = 1) =>
+ cy.get(THREAT_ITEM_ENTRY_DELETE_BUTTON).eq(row - 1);
+
+/** Returns the indicator matches AND button for the mapping */
+export const getIndicatorAndButton = () => cy.get(THREAT_MATCH_AND_BUTTON);
+
+/** Returns the indicator matches OR button for the mapping */
+export const getIndicatorOrButton = () => cy.get(THREAT_MATCH_OR_BUTTON);
+
+/** Returns the invalid match content. */
+export const getIndicatorInvalidationText = () => cy.contains(INVALID_MATCH_CONTENT);
+
+/** Returns that at least one valid match is required content */
+export const getIndicatorAtLeastOneInvalidationText = () => cy.contains(AT_LEAST_ONE_VALID_MATCH);
+
+/** Returns that at least one index pattern is required content */
+export const getIndexPatternInvalidationText = () => cy.contains(AT_LEAST_ONE_INDEX_PATTERN);
+
+/** Returns the continue button on the step of about */
+export const getAboutContinueButton = () => cy.get(ABOUT_CONTINUE_BTN);
+
+/** Returns the continue button on the step of define */
+export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON);
+
+/** Returns the indicator index pattern */
+export const getIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(0);
+
+/** Returns the indicator's indicator index */
+export const getIndicatorIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(2);
+
+/** Returns the index pattern's clear button */
+export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN);
+
+/** Returns the custom query input */
+export const getCustomQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(0);
+
+/** Returns the custom query input */
+export const getCustomIndicatorQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(1);
+
+/** Returns custom query required content */
+export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQUIRED);
+
+/**
+ * Fills in the define indicator match rules and then presses the continue button
+ * @param rule The rule to use to fill in everything
+ */
export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => {
- const INDEX_PATTERNS = 0;
- const INDICATOR_INDEX_PATTERN = 2;
- const INDICATOR_MAPPING = 3;
- const INDICATOR_INDEX_FIELD = 4;
-
- cy.get(COMBO_BOX_CLEAR_BTN).click();
- cy.get(COMBO_BOX_INPUT).eq(INDEX_PATTERNS).type(`${rule.index}{enter}`);
- cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_PATTERN).type(`${rule.indicatorIndexPattern}{enter}`);
- cy.get(COMBO_BOX_INPUT).eq(INDICATOR_MAPPING).type(`${rule.indicatorMapping}{enter}`);
- cy.get(COMBO_BOX_RESULT).first().click();
- cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_FIELD).type(`${rule.indicatorIndexField}{enter}`);
- cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
+ fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern);
+ fillIndicatorMatchRow({
+ indexField: rule.indicatorMapping,
+ indicatorIndexField: rule.indicatorIndexField,
+ });
+ getDefineContinueButton().should('exist').click({ force: true });
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
};
@@ -304,7 +424,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu
cy.get(ANOMALY_THRESHOLD_INPUT).type(`{selectall}${machineLearningRule.anomalyScoreThreshold}`, {
force: true,
});
- cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
+ getDefineContinueButton().should('exist').click({ force: true });
cy.get(MACHINE_LEARNING_DROPDOWN).should('not.exist');
};
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx
index 36033c358766d..ce6ca7ebc22dd 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx
@@ -22,6 +22,7 @@ describe('EntryItem', () => {
const wrapper = mount(
{
const wrapper = mount(
{
expect(mockOnChange).toHaveBeenCalledWith(
{
+ id: '123',
field: 'machine.os',
type: 'mapping',
value: 'ip',
@@ -97,6 +100,7 @@ describe('EntryItem', () => {
const wrapper = mount(
{
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: 'is not' }]);
- expect(mockOnChange).toHaveBeenCalledWith({ field: 'ip', type: 'mapping', value: '' }, 0);
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { id: '123', field: 'ip', type: 'mapping', value: '' },
+ 0
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx
index c99e63ff4eda0..51b724bff2e5d 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx
@@ -75,7 +75,11 @@ export const EntryItem: React.FC = ({
);
} else {
- return comboBox;
+ return (
+
+ {comboBox}
+
+ );
}
}, [handleFieldChange, indexPattern, entry, showLabel]);
@@ -101,7 +105,11 @@ export const EntryItem: React.FC = ({
);
} else {
- return comboBox;
+ return (
+
+ {comboBox}
+
+ );
}
}, [handleThreatFieldChange, threatIndexPatterns, entry, showLabel]);
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx
index b4f97808b54c4..b3a74c7697715 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx
@@ -21,6 +21,10 @@ import {
} from './helpers';
import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types';
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
const getMockIndexPattern = (): IndexPattern =>
({
id: '1234',
@@ -29,6 +33,7 @@ const getMockIndexPattern = (): IndexPattern =>
} as IndexPattern);
const getMockEntry = (): FormattedEntry => ({
+ id: '123',
field: getField('ip'),
value: getField('ip'),
type: 'mapping',
@@ -42,6 +47,7 @@ describe('Helpers', () => {
afterEach(() => {
moment.tz.setDefault('Browser');
+ jest.clearAllMocks();
});
describe('#getFormattedEntry', () => {
@@ -70,6 +76,7 @@ describe('Helpers', () => {
const output = getFormattedEntry(payloadIndexPattern, payloadIndexPattern, payloadItem, 0);
const expected: FormattedEntry = {
entryIndex: 0,
+ id: '123',
field: {
name: 'machine.os.raw.text',
type: 'string',
@@ -94,6 +101,7 @@ describe('Helpers', () => {
const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems);
const expected: FormattedEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: undefined,
value: undefined,
@@ -109,6 +117,7 @@ describe('Helpers', () => {
const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems);
const expected: FormattedEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: {
name: 'machine.os',
@@ -134,6 +143,7 @@ describe('Helpers', () => {
const output = getFormattedEntries(payloadIndexPattern, threatIndexPattern, payloadItems);
const expected: FormattedEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: {
name: 'machine.os',
@@ -170,6 +180,7 @@ describe('Helpers', () => {
const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems);
const expected: FormattedEntry[] = [
{
+ id: '123',
field: {
name: 'machine.os',
type: 'string',
@@ -194,6 +205,7 @@ describe('Helpers', () => {
entryIndex: 0,
},
{
+ id: '123',
field: {
name: 'ip',
type: 'ip',
@@ -249,9 +261,10 @@ describe('Helpers', () => {
const payloadItem = getMockEntry();
const payloadIFieldType = getField('ip');
const output = getEntryOnFieldChange(payloadItem, payloadIFieldType);
- const expected: { updatedEntry: Entry; index: number } = {
+ const expected: { updatedEntry: Entry & { id: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
field: 'ip',
type: 'mapping',
value: 'ip',
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
index 349dae76301d4..90a996c06e492 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import uuid from 'uuid';
import {
ThreatMap,
threatMap,
@@ -12,6 +13,7 @@ import {
import { IndexPattern, IFieldType } from '../../../../../../../src/plugins/data/common';
import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types';
+import { addIdToItem } from '../../utils/add_remove_id_to_item';
/**
* Formats the entry into one that is easily usable for the UI.
@@ -24,7 +26,8 @@ export const getFormattedEntry = (
indexPattern: IndexPattern,
threatIndexPatterns: IndexPattern,
item: Entry,
- itemIndex: number
+ itemIndex: number,
+ uuidGen: () => string = uuid.v4
): FormattedEntry => {
const { fields } = indexPattern;
const { fields: threatFields } = threatIndexPatterns;
@@ -34,7 +37,9 @@ export const getFormattedEntry = (
const [threatFoundField] = threatFields.filter(
({ name }) => threatField != null && threatField === name
);
+ const maybeId: typeof item & { id?: string } = item;
return {
+ id: maybeId.id ?? uuidGen(),
field: foundField,
type: 'mapping',
value: threatFoundField,
@@ -90,10 +95,11 @@ export const getEntryOnFieldChange = (
const { entryIndex } = item;
return {
updatedEntry: {
+ id: item.id,
field: newField != null ? newField.name : '',
type: 'mapping',
value: item.value != null ? item.value.name : '',
- },
+ } as Entry, // Cast to Entry since id is only used as a react key prop and can be ignored elsewhere
index: entryIndex,
};
};
@@ -112,30 +118,33 @@ export const getEntryOnThreatFieldChange = (
const { entryIndex } = item;
return {
updatedEntry: {
+ id: item.id,
field: item.field != null ? item.field.name : '',
type: 'mapping',
value: newField != null ? newField.name : '',
- },
+ } as Entry, // Cast to Entry since id is only used as a react key prop and can be ignored elsewhere
index: entryIndex,
};
};
-export const getDefaultEmptyEntry = (): EmptyEntry => ({
- field: '',
- type: 'mapping',
- value: '',
-});
+export const getDefaultEmptyEntry = (): EmptyEntry => {
+ return addIdToItem({
+ field: '',
+ type: 'mapping',
+ value: '',
+ });
+};
export const getNewItem = (): ThreatMap => {
- return {
+ return addIdToItem({
entries: [
- {
+ addIdToItem({
field: '',
type: 'mapping',
value: '',
- },
+ }),
],
- };
+ });
};
export const filterItems = (items: ThreatMapEntries[]): ThreatMapping => {
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx
index d3936e10bd877..8aa4af21b03cc 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx
@@ -158,43 +158,45 @@ export const ThreatMatchComponent = ({
}, []);
return (
- {entries.map((entryListItem, index) => (
-
-
- {index !== 0 &&
- (andLogicIncluded ? (
-
-
-
-
-
-
-
-
-
-
- ) : (
-
-
-
- ))}
-
-
-
-
-
- ))}
+ {entries.map((entryListItem, index) => {
+ const key = (entryListItem as typeof entryListItem & { id?: string }).id ?? `${index}`;
+ return (
+
+
+ {index !== 0 &&
+ (andLogicIncluded ? (
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ ))}
+
+
+
+
+
+ );
+ })}
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx
index 90492bc46e2b0..66af24025656e 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx
@@ -68,7 +68,6 @@ describe('ListItemComponent', () => {
({ eui: euiLightVars, darkMode: false })}>
{
({ eui: euiLightVars, darkMode: false })}>
{
({ eui: euiLightVars, darkMode: false })}>
{
({ eui: euiLightVars, darkMode: false })}>
{
const wrapper = mount(
{
const wrapper = mount(
{
const wrapper = mount(
{
const wrapper = mount(
{
const wrapper = mount(
(
({
listItem,
- listId,
listItemIndex,
indexPattern,
threatIndexPatterns,
@@ -88,7 +86,7 @@ export const ListItemComponent = React.memo(
{entries.map((item, index) => (
-
+
({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
const initialState: State = {
andLogicIncluded: false,
entries: [],
@@ -22,6 +26,10 @@ const getEntry = (): ThreatMapEntry => ({
});
describe('reducer', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
describe('#setEntries', () => {
test('should return "andLogicIncluded" ', () => {
const update = reducer()(initialState, {
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts
index 0cbd885db2d54..f3af5faaed25c 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts
@@ -7,6 +7,7 @@ import { ThreatMap, ThreatMapEntry } from '../../../../common/detection_engine/s
import { IFieldType } from '../../../../../../../src/plugins/data/common';
export interface FormattedEntry {
+ id: string;
field: IFieldType | undefined;
type: 'mapping';
value: IFieldType | undefined;
diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts
new file mode 100644
index 0000000000000..fa067a53f2573
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { addIdToItem, removeIdFromItem } from './add_remove_id_to_item';
+
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
+describe('add_remove_id_to_item', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('addIdToItem', () => {
+ test('it adds an id to an empty item', () => {
+ expect(addIdToItem({})).toEqual({ id: '123' });
+ });
+
+ test('it adds a complex object', () => {
+ expect(
+ addIdToItem({
+ field: '',
+ type: 'mapping',
+ value: '',
+ })
+ ).toEqual({
+ id: '123',
+ field: '',
+ type: 'mapping',
+ value: '',
+ });
+ });
+
+ test('it adds an id to an existing item', () => {
+ expect(addIdToItem({ test: '456' })).toEqual({ id: '123', test: '456' });
+ });
+
+ test('it does not change the id if it already exists', () => {
+ expect(addIdToItem({ id: '456' })).toEqual({ id: '456' });
+ });
+
+ test('it returns the same reference if it has an id already', () => {
+ const obj = { id: '456' };
+ expect(addIdToItem(obj)).toBe(obj);
+ });
+
+ test('it returns a new reference if it adds an id to an item', () => {
+ const obj = { test: '456' };
+ expect(addIdToItem(obj)).not.toBe(obj);
+ });
+ });
+
+ describe('removeIdFromItem', () => {
+ test('it removes an id from an item', () => {
+ expect(removeIdFromItem({ id: '456' })).toEqual({});
+ });
+
+ test('it returns a new reference if it removes an id from an item', () => {
+ const obj = { id: '123', test: '456' };
+ expect(removeIdFromItem(obj)).not.toBe(obj);
+ });
+
+ test('it does not effect an item without an id', () => {
+ expect(removeIdFromItem({ test: '456' })).toEqual({ test: '456' });
+ });
+
+ test('it returns the same reference if it does not have an id already', () => {
+ const obj = { test: '456' };
+ expect(removeIdFromItem(obj)).toBe(obj);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts
new file mode 100644
index 0000000000000..a74cf8680fa48
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import uuid from 'uuid';
+
+/**
+ * This is useful for when you have arrays without an ID and need to add one for
+ * ReactJS keys. I break the types slightly by introducing an id to an arbitrary item
+ * but then cast it back to the regular type T.
+ * Usage of this could be considered tech debt as I am adding an ID when the backend
+ * could be doing the same thing but it depends on how you want to model your data and
+ * if you view modeling your data with id's to please ReactJS a good or bad thing.
+ * @param item The item to add an id to.
+ */
+type NotArray = T extends unknown[] ? never : T;
+export const addIdToItem = (item: NotArray): T => {
+ const maybeId: typeof item & { id?: string } = item;
+ if (maybeId.id != null) {
+ return item;
+ } else {
+ return { ...item, id: uuid.v4() };
+ }
+};
+
+/**
+ * This is to reverse the id you added to your arrays for ReactJS keys.
+ * @param item The item to remove the id from.
+ */
+export const removeIdFromItem = (
+ item: NotArray
+):
+ | T
+ | Pick<
+ T & {
+ id?: string | undefined;
+ },
+ Exclude
+ > => {
+ const maybeId: typeof item & { id?: string } = item;
+ if (maybeId.id != null) {
+ const { id, ...noId } = maybeId;
+ return noId;
+ } else {
+ return item;
+ }
+};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx
index b72dd3b2f84dd..191c3955caa9b 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx
@@ -50,7 +50,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => {
const abortCtrl = new AbortController();
setLoading(true);
- async function fetchData() {
+ const fetchData = async () => {
try {
const privilege = await getUserPrivilege({
signal: abortCtrl.signal,
@@ -89,15 +89,14 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => {
if (isSubscribed) {
setLoading(false);
}
- }
+ };
fetchData();
return () => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [dispatchToaster]);
return { loading, ...privilegeUser };
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
index 3bef1d8edd048..9022e3a32163c 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
@@ -46,7 +46,7 @@ export const useQueryAlerts = (
let isSubscribed = true;
const abortCtrl = new AbortController();
- async function fetchData() {
+ const fetchData = async () => {
try {
setLoading(true);
const alertResponse = await fetchQueryAlerts({
@@ -77,7 +77,7 @@ export const useQueryAlerts = (
if (isSubscribed) {
setLoading(false);
}
- }
+ };
fetchData();
return () => {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
index 5ebdb38b8dd5c..bfdc1d1ceee21 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
@@ -106,8 +106,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [dispatchToaster]);
return { loading, ...signalIndex };
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts
new file mode 100644
index 0000000000000..7821bb23a7ca3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { flow } from 'fp-ts/lib/function';
+import { addIdToItem, removeIdFromItem } from '../../../../common/utils/add_remove_id_to_item';
+import {
+ CreateRulesSchema,
+ UpdateRulesSchema,
+} from '../../../../../common/detection_engine/schemas/request';
+import { Rule } from './types';
+
+// These are a collection of transforms that are UI specific and useful for UI concerns
+// that are inserted between the API and the actual user interface. In some ways these
+// might be viewed as technical debt or to compensate for the differences and preferences
+// of how ReactJS might prefer data vs. how we want to model data. Each function should have
+// a description giving context around the transform.
+
+/**
+ * Transforms the output of rules to compensate for technical debt or UI concerns such as
+ * ReactJS preferences for having ids within arrays if the data is not modeled that way.
+ *
+ * If you add a new transform of the output called "myNewTransform" do it
+ * in the form of:
+ * flow(removeIdFromThreatMatchArray, myNewTransform)(rule)
+ *
+ * @param rule The rule to transform the output of
+ * @returns The rule transformed from the output
+ */
+export const transformOutput = (
+ rule: CreateRulesSchema | UpdateRulesSchema
+): CreateRulesSchema | UpdateRulesSchema => flow(removeIdFromThreatMatchArray)(rule);
+
+/**
+ * Transforms the output of rules to compensate for technical debt or UI concerns such as
+ * ReactJS preferences for having ids within arrays if the data is not modeled that way.
+ *
+ * If you add a new transform of the input called "myNewTransform" do it
+ * in the form of:
+ * flow(addIdToThreatMatchArray, myNewTransform)(rule)
+ *
+ * @param rule The rule to transform the output of
+ * @returns The rule transformed from the output
+ */
+export const transformInput = (rule: Rule): Rule => flow(addIdToThreatMatchArray)(rule);
+
+/**
+ * This adds an id to the incoming threat match arrays as ReactJS prefers to have
+ * an id added to them for use as a stable id. Later if we decide to change the data
+ * model to have id's within the array then this code should be removed. If not, then
+ * this code should stay as an adapter for ReactJS.
+ *
+ * This does break the type system slightly as we are lying a bit to the type system as we return
+ * the same rule as we have previously but are augmenting the arrays with an id which TypeScript
+ * doesn't mind us doing here. However, downstream you will notice that you have an id when the type
+ * does not indicate it. In that case just cast this temporarily if you're using the id. If you're not,
+ * you can ignore the id and just use the normal TypeScript with ReactJS.
+ *
+ * @param rule The rule to add an id to the threat matches.
+ * @returns rule The rule but with id added to the threat array and entries
+ */
+export const addIdToThreatMatchArray = (rule: Rule): Rule => {
+ if (rule.type === 'threat_match' && rule.threat_mapping != null) {
+ const threatMapWithId = rule.threat_mapping.map((mapping) => {
+ const newEntries = mapping.entries.map((entry) => addIdToItem(entry));
+ return addIdToItem({ entries: newEntries });
+ });
+ return { ...rule, threat_mapping: threatMapWithId };
+ } else {
+ return rule;
+ }
+};
+
+/**
+ * This removes an id from the threat match arrays as ReactJS prefers to have
+ * an id added to them for use as a stable id. Later if we decide to change the data
+ * model to have id's within the array then this code should be removed. If not, then
+ * this code should stay as an adapter for ReactJS.
+ *
+ * @param rule The rule to remove an id from the threat matches.
+ * @returns rule The rule but with id removed from the threat array and entries
+ */
+export const removeIdFromThreatMatchArray = (
+ rule: CreateRulesSchema | UpdateRulesSchema
+): CreateRulesSchema | UpdateRulesSchema => {
+ if (rule.type === 'threat_match' && rule.threat_mapping != null) {
+ const threatMapWithoutId = rule.threat_mapping.map((mapping) => {
+ const newEntries = mapping.entries.map((entry) => removeIdFromItem(entry));
+ const newMapping = removeIdFromItem(mapping);
+ return { ...newMapping, entries: newEntries };
+ });
+ return { ...rule, threat_mapping: threatMapWithoutId };
+ } else {
+ return rule;
+ }
+};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
index 2bbd27994fc77..fe8e0fd8ceb97 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
@@ -11,6 +11,7 @@ import { CreateRulesSchema } from '../../../../../common/detection_engine/schema
import { createRule } from './api';
import * as i18n from './translations';
+import { transformOutput } from './transforms';
interface CreateRuleReturn {
isLoading: boolean;
@@ -29,11 +30,11 @@ export const useCreateRule = (): ReturnCreateRule => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setIsSaved(false);
- async function saveRule() {
+ const saveRule = async () => {
if (rule != null) {
try {
setIsLoading(true);
- await createRule({ rule, signal: abortCtrl.signal });
+ await createRule({ rule: transformOutput(rule), signal: abortCtrl.signal });
if (isSubscribed) {
setIsSaved(true);
}
@@ -46,15 +47,14 @@ export const useCreateRule = (): ReturnCreateRule => {
setIsLoading(false);
}
}
- }
+ };
saveRule();
return () => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [rule]);
+ }, [rule, dispatchToaster]);
return [{ isLoading, isSaved }, setRule];
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
index d83d4e0caa977..bdbe13af40151 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
@@ -262,8 +262,14 @@ export const usePrePackagedRules = ({
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]);
+ }, [
+ canUserCRUD,
+ hasIndexWrite,
+ isAuthenticated,
+ hasEncryptionKey,
+ isSignalIndexExists,
+ dispatchToaster,
+ ]);
const prePackagedRuleStatus = useMemo(
() =>
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx
index 706c2645a4ddd..3b84558d344e7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx
@@ -8,6 +8,7 @@ import { useEffect, useState } from 'react';
import { errorToToaster, useStateToaster } from '../../../../common/components/toasters';
import { fetchRuleById } from './api';
+import { transformInput } from './transforms';
import * as i18n from './translations';
import { Rule } from './types';
@@ -28,13 +29,15 @@ export const useRule = (id: string | undefined): ReturnRule => {
let isSubscribed = true;
const abortCtrl = new AbortController();
- async function fetchData(idToFetch: string) {
+ const fetchData = async (idToFetch: string) => {
try {
setLoading(true);
- const ruleResponse = await fetchRuleById({
- id: idToFetch,
- signal: abortCtrl.signal,
- });
+ const ruleResponse = transformInput(
+ await fetchRuleById({
+ id: idToFetch,
+ signal: abortCtrl.signal,
+ })
+ );
if (isSubscribed) {
setRule(ruleResponse);
}
@@ -47,7 +50,7 @@ export const useRule = (id: string | undefined): ReturnRule => {
if (isSubscribed) {
setLoading(false);
}
- }
+ };
if (id != null) {
fetchData(id);
}
@@ -55,8 +58,7 @@ export const useRule = (id: string | undefined): ReturnRule => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [id]);
+ }, [id, dispatchToaster]);
return [loading, rule];
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx
index fbca46097dcd9..48bfe71b4722b 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx
@@ -6,12 +6,14 @@
import { useEffect, useCallback } from 'react';
+import { flow } from 'fp-ts/lib/function';
import { useAsync, withOptionalSignal } from '../../../../shared_imports';
import { useHttp } from '../../../../common/lib/kibana';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { pureFetchRuleById } from './api';
import { Rule } from './types';
import * as i18n from './translations';
+import { transformInput } from './transforms';
export interface UseRuleAsync {
error: unknown;
@@ -20,11 +22,15 @@ export interface UseRuleAsync {
rule: Rule | null;
}
-const _fetchRule = withOptionalSignal(pureFetchRuleById);
-const _useRuleAsync = () => useAsync(_fetchRule);
+const _fetchRule = flow(withOptionalSignal(pureFetchRuleById), async (rule: Promise) =>
+ transformInput(await rule)
+);
+
+/** This does not use "_useRuleAsyncInternal" as that would deactivate the useHooks linter rule, so instead it has the word "Internal" post-pended */
+const useRuleAsyncInternal = () => useAsync(_fetchRule);
export const useRuleAsync = (ruleId: string): UseRuleAsync => {
- const { start, loading, result, error } = _useRuleAsync();
+ const { start, loading, result, error } = useRuleAsyncInternal();
const http = useHttp();
const { addError } = useAppToasts();
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
index ddf50e9edae51..2bec8f9a2d0a2 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
@@ -64,8 +64,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus =
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [id]);
+ }, [id, dispatchToaster]);
return [loading, ruleStatus, fetchRuleStatus.current];
};
@@ -122,8 +121,7 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [rules]);
+ }, [rules, dispatchToaster]);
return { loading, rulesStatuses };
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx
index 038f974e1394e..bab419813e1aa 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx
@@ -26,7 +26,7 @@ export const useTags = (): ReturnTags => {
let isSubscribed = true;
const abortCtrl = new AbortController();
- async function fetchData() {
+ const fetchData = async () => {
setLoading(true);
try {
const fetchTagsResult = await fetchTags({
@@ -44,7 +44,7 @@ export const useTags = (): ReturnTags => {
if (isSubscribed) {
setLoading(false);
}
- }
+ };
fetchData();
reFetchTags.current = fetchData;
@@ -53,8 +53,7 @@ export const useTags = (): ReturnTags => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [dispatchToaster]);
return [loading, tags, reFetchTags.current];
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx
index a437974e93ba3..729336b697e4d 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx
@@ -9,6 +9,8 @@ import { useEffect, useState, Dispatch } from 'react';
import { errorToToaster, useStateToaster } from '../../../../common/components/toasters';
import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request';
+import { transformOutput } from './transforms';
+
import { updateRule } from './api';
import * as i18n from './translations';
@@ -29,11 +31,11 @@ export const useUpdateRule = (): ReturnUpdateRule => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setIsSaved(false);
- async function saveRule() {
+ const saveRule = async () => {
if (rule != null) {
try {
setIsLoading(true);
- await updateRule({ rule, signal: abortCtrl.signal });
+ await updateRule({ rule: transformOutput(rule), signal: abortCtrl.signal });
if (isSubscribed) {
setIsSaved(true);
}
@@ -46,15 +48,14 @@ export const useUpdateRule = (): ReturnUpdateRule => {
setIsLoading(false);
}
}
- }
+ };
saveRule();
return () => {
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [rule]);
+ }, [rule, dispatchToaster]);
return [{ isLoading, isSaved }, setRule];
};
From 05b7107ff2274987b4c37889813cd4e685eca184 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Sat, 30 Jan 2021 10:49:59 +0100
Subject: [PATCH 05/18] Add APM API tests dir to CODEOWNERS (#89573)
---
.github/CODEOWNERS | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 3343544d57fad..9e31bd31b4037 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -66,6 +66,7 @@
# APM
/x-pack/plugins/apm/ @elastic/apm-ui
/x-pack/test/functional/apps/apm/ @elastic/apm-ui
+/x-pack/test/apm_api_integration/ @elastic/apm-ui
/src/plugins/apm_oss/ @elastic/apm-ui
/src/apm.js @elastic/kibana-core @vigneshshanmugam
/packages/kbn-apm-config-loader/ @elastic/kibana-core @vigneshshanmugam
@@ -80,6 +81,7 @@
/x-pack/plugins/apm/server/lib/rum_client @elastic/uptime
/x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime
/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime
+/x-pack/test/apm_api_integration/tests/csm/ @elastic/uptime
# Beats
/x-pack/plugins/beats_management/ @elastic/beats
From 52f54030c356447f6896e603b60350be97389fd2 Mon Sep 17 00:00:00 2001
From: "Devin W. Hurley"
Date: Sat, 30 Jan 2021 08:25:45 -0500
Subject: [PATCH 06/18] [Security Solution] [Detections] rename gap column and
delete "last lookback date" column from monitoring table (#89801)
---
.../detection_engine/rules/all/columns.tsx | 27 ++++++++++---------
.../detection_engine/rules/translations.ts | 2 +-
2 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index 0d585b4463815..86f24594fc57e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -356,19 +356,20 @@ export const getMonitoringColumns = (
truncateText: true,
width: '14%',
},
- {
- field: 'current_status.last_look_back_date',
- name: i18n.COLUMN_LAST_LOOKBACK_DATE,
- render: (value: RuleStatus['current_status']['last_look_back_date']) => {
- return value == null ? (
- getEmptyTagValue()
- ) : (
-
- );
- },
- truncateText: true,
- width: '16%',
- },
+ // hiding this field until after 7.11 release
+ // {
+ // field: 'current_status.last_look_back_date',
+ // name: i18n.COLUMN_LAST_LOOKBACK_DATE,
+ // render: (value: RuleStatus['current_status']['last_look_back_date']) => {
+ // return value == null ? (
+ // getEmptyTagValue()
+ // ) : (
+ //
+ // );
+ // },
+ // truncateText: true,
+ // width: '16%',
+ // },
{
field: 'current_status.status_date',
name: i18n.COLUMN_LAST_COMPLETE_RUN,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
index 2d993c7be08b0..f7066cd42e4c1 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
@@ -353,7 +353,7 @@ export const COLUMN_QUERY_TIMES = i18n.translate(
export const COLUMN_GAP = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.columns.gap',
{
- defaultMessage: 'Gap (if any)',
+ defaultMessage: 'Last Gap (if any)',
}
);
From 841ab704b8e50986730a32e68f9afc3ac28b92cd Mon Sep 17 00:00:00 2001
From: Liza Katz
Date: Sun, 31 Jan 2021 12:16:46 +0200
Subject: [PATCH 07/18] [Search Sessions] Improve search session errors
(#88613)
* Detect ESError correctly
Fix bfetch error (was recognized as unknown error)
Make sure handleSearchError always returns an error object.
* fix tests and improve types
* type
* normalize search error response format for search and bsearch
* type
* Added es search exception examples
* Normalize and validate errors thrown from oss es_search_strategy
Validate abort
* Added tests for search service error handling
* Update msearch tests to test for errors
* Moved bsearch route to routes folder
Adjusted bsearch response format
Added verification of error's root cause
* Align painless error object
* eslint
* Add to seach interceptor tests
* add json to tsconfig
* docs
* updated xpack search strategy tests
* oops
* license header
* Add test for xpack painless error format
* doc
* Fix bsearch test potential flakiness
* code review
* fix
* code review 2
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
...lic.searchinterceptor.handlesearcherror.md | 4 +-
...public.searchtimeouterror._constructor_.md | 4 +-
.../test_data/illegal_argument_exception.json | 14 ++
.../test_data/index_not_found_exception.json | 21 ++
.../test_data/json_e_o_f_exception.json | 14 ++
.../search/test_data/parsing_exception.json | 17 ++
.../resource_not_found_exception.json | 13 +
.../search_phase_execution_exception.json | 52 ++++
.../test_data/x_content_parse_exception.json | 17 ++
src/plugins/data/public/public.api.md | 7 +-
.../public/search/errors/es_error.test.tsx | 19 +-
.../data/public/search/errors/es_error.tsx | 8 +-
.../search/errors/painless_error.test.tsx | 42 ++++
.../public/search/errors/painless_error.tsx | 10 +-
.../public/search/errors/timeout_error.tsx | 2 +-
.../data/public/search/errors/types.ts | 72 +++---
.../data/public/search/errors/utils.ts | 16 +-
.../public/search/search_interceptor.test.ts | 74 +++---
.../data/public/search/search_interceptor.ts | 23 +-
.../es_search/es_search_strategy.test.ts | 161 ++++++++++--
.../search/es_search/es_search_strategy.ts | 31 ++-
.../data/server/search/routes/bsearch.ts | 65 +++++
.../data/server/search/routes/call_msearch.ts | 36 +--
.../data/server/search/routes/msearch.test.ts | 58 ++++-
.../data/server/search/routes/search.test.ts | 99 ++++++--
.../data/server/search/search_service.ts | 55 +----
src/plugins/data/tsconfig.json | 2 +-
.../kibana_utils/common/errors/index.ts | 1 +
.../kibana_utils/common/errors/types.ts | 12 +
src/plugins/kibana_utils/server/index.ts | 2 +-
.../server/report_server_error.ts | 29 ++-
test/api_integration/apis/search/bsearch.ts | 172 +++++++++++++
test/api_integration/apis/search/index.ts | 1 +
.../apis/search/painless_err_req.ts | 44 ++++
test/api_integration/apis/search/search.ts | 81 ++++++-
.../apis/search/verify_error.ts | 27 +++
.../search_phase_execution_exception.json | 229 ++++++++++++++++++
.../public/search/search_interceptor.test.ts | 41 +++-
.../server/search/es_search_strategy.test.ts | 101 ++++++++
.../server/search/es_search_strategy.ts | 79 ++++--
x-pack/plugins/data_enhanced/tsconfig.json | 3 +-
.../api_integration/apis/search/search.ts | 36 ++-
42 files changed, 1499 insertions(+), 295 deletions(-)
create mode 100644 src/plugins/data/common/search/test_data/illegal_argument_exception.json
create mode 100644 src/plugins/data/common/search/test_data/index_not_found_exception.json
create mode 100644 src/plugins/data/common/search/test_data/json_e_o_f_exception.json
create mode 100644 src/plugins/data/common/search/test_data/parsing_exception.json
create mode 100644 src/plugins/data/common/search/test_data/resource_not_found_exception.json
create mode 100644 src/plugins/data/common/search/test_data/search_phase_execution_exception.json
create mode 100644 src/plugins/data/common/search/test_data/x_content_parse_exception.json
create mode 100644 src/plugins/data/public/search/errors/painless_error.test.tsx
create mode 100644 src/plugins/data/server/search/routes/bsearch.ts
create mode 100644 src/plugins/kibana_utils/common/errors/types.ts
create mode 100644 test/api_integration/apis/search/bsearch.ts
create mode 100644 test/api_integration/apis/search/painless_err_req.ts
create mode 100644 test/api_integration/apis/search/verify_error.ts
create mode 100644 x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md
index b5ac4a4e53887..5f8966f0227ac 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md
@@ -7,14 +7,14 @@
Signature:
```typescript
-protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
+protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| e | any
| |
+| e | KibanaServerError | AbortError
| |
| timeoutSignal | AbortSignal
| |
| options | ISearchOptions
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md
index 1c6370c7d0356..b4eecca665e82 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md
@@ -9,13 +9,13 @@ Constructs a new instance of the `SearchTimeoutError` class
Signature:
```typescript
-constructor(err: Error, mode: TimeoutErrorMode);
+constructor(err: Record, mode: TimeoutErrorMode);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| err | Error
| |
+| err | Record<string, any>
| |
| mode | TimeoutErrorMode
| |
diff --git a/src/plugins/data/common/search/test_data/illegal_argument_exception.json b/src/plugins/data/common/search/test_data/illegal_argument_exception.json
new file mode 100644
index 0000000000000..ae48468abc209
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/illegal_argument_exception.json
@@ -0,0 +1,14 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "illegal_argument_exception",
+ "reason" : "failed to parse setting [timeout] with value [1] as a time value: unit is missing or unrecognized"
+ }
+ ],
+ "type" : "illegal_argument_exception",
+ "reason" : "failed to parse setting [timeout] with value [1] as a time value: unit is missing or unrecognized"
+ },
+ "status" : 400
+ }
+
\ No newline at end of file
diff --git a/src/plugins/data/common/search/test_data/index_not_found_exception.json b/src/plugins/data/common/search/test_data/index_not_found_exception.json
new file mode 100644
index 0000000000000..dc892d95ae397
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/index_not_found_exception.json
@@ -0,0 +1,21 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "index_not_found_exception",
+ "reason" : "no such index [poop]",
+ "resource.type" : "index_or_alias",
+ "resource.id" : "poop",
+ "index_uuid" : "_na_",
+ "index" : "poop"
+ }
+ ],
+ "type" : "index_not_found_exception",
+ "reason" : "no such index [poop]",
+ "resource.type" : "index_or_alias",
+ "resource.id" : "poop",
+ "index_uuid" : "_na_",
+ "index" : "poop"
+ },
+ "status" : 404
+}
diff --git a/src/plugins/data/common/search/test_data/json_e_o_f_exception.json b/src/plugins/data/common/search/test_data/json_e_o_f_exception.json
new file mode 100644
index 0000000000000..88134e1c6ea03
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/json_e_o_f_exception.json
@@ -0,0 +1,14 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "json_e_o_f_exception",
+ "reason" : "Unexpected end-of-input: expected close marker for Object (start marker at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 1])\n at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 2]"
+ }
+ ],
+ "type" : "json_e_o_f_exception",
+ "reason" : "Unexpected end-of-input: expected close marker for Object (start marker at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 1])\n at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 2]"
+ },
+ "status" : 400
+ }
+
\ No newline at end of file
diff --git a/src/plugins/data/common/search/test_data/parsing_exception.json b/src/plugins/data/common/search/test_data/parsing_exception.json
new file mode 100644
index 0000000000000..725a847aa0e3f
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/parsing_exception.json
@@ -0,0 +1,17 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "parsing_exception",
+ "reason" : "[terms] query does not support [ohno]",
+ "line" : 4,
+ "col" : 17
+ }
+ ],
+ "type" : "parsing_exception",
+ "reason" : "[terms] query does not support [ohno]",
+ "line" : 4,
+ "col" : 17
+ },
+ "status" : 400
+}
diff --git a/src/plugins/data/common/search/test_data/resource_not_found_exception.json b/src/plugins/data/common/search/test_data/resource_not_found_exception.json
new file mode 100644
index 0000000000000..7f2a3b2e6e143
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/resource_not_found_exception.json
@@ -0,0 +1,13 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "resource_not_found_exception",
+ "reason" : "FlZlSXp6dkd3UXdHZjhsalVtVHBnYkEdYjNIWDhDOTZRN3ExemdmVkx4RXNQQToxMjc2ODk="
+ }
+ ],
+ "type" : "resource_not_found_exception",
+ "reason" : "FlZlSXp6dkd3UXdHZjhsalVtVHBnYkEdYjNIWDhDOTZRN3ExemdmVkx4RXNQQToxMjc2ODk="
+ },
+ "status" : 404
+}
diff --git a/src/plugins/data/common/search/test_data/search_phase_execution_exception.json b/src/plugins/data/common/search/test_data/search_phase_execution_exception.json
new file mode 100644
index 0000000000000..ff6879f2b8960
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/search_phase_execution_exception.json
@@ -0,0 +1,52 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "script_exception",
+ "reason" : "compile error",
+ "script_stack" : [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script" : "invalid",
+ "lang" : "painless",
+ "position" : {
+ "offset" : 0,
+ "start" : 0,
+ "end" : 7
+ }
+ }
+ ],
+ "type" : "search_phase_execution_exception",
+ "reason" : "all shards failed",
+ "phase" : "query",
+ "grouped" : true,
+ "failed_shards" : [
+ {
+ "shard" : 0,
+ "index" : ".kibana_11",
+ "node" : "b3HX8C96Q7q1zgfVLxEsPA",
+ "reason" : {
+ "type" : "script_exception",
+ "reason" : "compile error",
+ "script_stack" : [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script" : "invalid",
+ "lang" : "painless",
+ "position" : {
+ "offset" : 0,
+ "start" : 0,
+ "end" : 7
+ },
+ "caused_by" : {
+ "type" : "illegal_argument_exception",
+ "reason" : "cannot resolve symbol [invalid]"
+ }
+ }
+ }
+ ]
+ },
+ "status" : 400
+}
diff --git a/src/plugins/data/common/search/test_data/x_content_parse_exception.json b/src/plugins/data/common/search/test_data/x_content_parse_exception.json
new file mode 100644
index 0000000000000..cd6e1cb2c5977
--- /dev/null
+++ b/src/plugins/data/common/search/test_data/x_content_parse_exception.json
@@ -0,0 +1,17 @@
+{
+ "error" : {
+ "root_cause" : [
+ {
+ "type" : "x_content_parse_exception",
+ "reason" : "[5:13] [script] failed to parse object"
+ }
+ ],
+ "type" : "x_content_parse_exception",
+ "reason" : "[5:13] [script] failed to parse object",
+ "caused_by" : {
+ "type" : "json_parse_exception",
+ "reason" : "Unexpected character (''' (code 39)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (org.elasticsearch.common.bytes.AbstractBytesReference$BytesReferenceStreamInput); line: 5, column: 24]"
+ }
+ },
+ "status" : 400
+}
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 5b1462e5d506b..f533af2db9672 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -2282,8 +2282,11 @@ export class SearchInterceptor {
protected readonly deps: SearchInterceptorDeps;
// (undocumented)
protected getTimeoutMode(): TimeoutErrorMode;
+ // Warning: (ae-forgotten-export) The symbol "KibanaServerError" needs to be exported by the entry point index.d.ts
+ // Warning: (ae-forgotten-export) The symbol "AbortError" needs to be exported by the entry point index.d.ts
+ //
// (undocumented)
- protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
+ protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
// @internal
protected pendingCount$: BehaviorSubject;
// @internal (undocumented)
@@ -2453,7 +2456,7 @@ export interface SearchSourceFields {
//
// @public
export class SearchTimeoutError extends KbnError {
- constructor(err: Error, mode: TimeoutErrorMode);
+ constructor(err: Record, mode: TimeoutErrorMode);
// (undocumented)
getErrorMessage(application: ApplicationStart): JSX.Element;
// (undocumented)
diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx
index adb422c1d18e7..6a4cb9c494b4f 100644
--- a/src/plugins/data/public/search/errors/es_error.test.tsx
+++ b/src/plugins/data/public/search/errors/es_error.test.tsx
@@ -7,23 +7,22 @@
*/
import { EsError } from './es_error';
-import { IEsError } from './types';
describe('EsError', () => {
it('contains the same body as the wrapped error', () => {
const error = {
- body: {
- attributes: {
- error: {
- type: 'top_level_exception_type',
- reason: 'top-level reason',
- },
+ statusCode: 500,
+ message: 'nope',
+ attributes: {
+ error: {
+ type: 'top_level_exception_type',
+ reason: 'top-level reason',
},
},
- } as IEsError;
+ } as any;
const esError = new EsError(error);
- expect(typeof esError.body).toEqual('object');
- expect(esError.body).toEqual(error.body);
+ expect(typeof esError.attributes).toEqual('object');
+ expect(esError.attributes).toEqual(error.attributes);
});
});
diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx
index fff06d2e1bfb6..d241eecfd8d5d 100644
--- a/src/plugins/data/public/search/errors/es_error.tsx
+++ b/src/plugins/data/public/search/errors/es_error.tsx
@@ -11,19 +11,19 @@ import { EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { ApplicationStart } from 'kibana/public';
import { KbnError } from '../../../../kibana_utils/common';
import { IEsError } from './types';
-import { getRootCause, getTopLevelCause } from './utils';
+import { getRootCause } from './utils';
export class EsError extends KbnError {
- readonly body: IEsError['body'];
+ readonly attributes: IEsError['attributes'];
constructor(protected readonly err: IEsError) {
super('EsError');
- this.body = err.body;
+ this.attributes = err.attributes;
}
public getErrorMessage(application: ApplicationStart) {
const rootCause = getRootCause(this.err)?.reason;
- const topLevelCause = getTopLevelCause(this.err)?.reason;
+ const topLevelCause = this.attributes?.reason;
const cause = rootCause ?? topLevelCause;
return (
diff --git a/src/plugins/data/public/search/errors/painless_error.test.tsx b/src/plugins/data/public/search/errors/painless_error.test.tsx
new file mode 100644
index 0000000000000..929f25e234a60
--- /dev/null
+++ b/src/plugins/data/public/search/errors/painless_error.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { coreMock } from '../../../../../core/public/mocks';
+const startMock = coreMock.createStart();
+
+import { mount } from 'enzyme';
+import { PainlessError } from './painless_error';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import * as searchPhaseException from '../../../common/search/test_data/search_phase_execution_exception.json';
+
+describe('PainlessError', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Should show reason and code', () => {
+ const e = new PainlessError({
+ statusCode: 400,
+ message: 'search_phase_execution_exception',
+ attributes: searchPhaseException.error,
+ });
+ const component = mount(e.getErrorMessage(startMock.application));
+
+ const scriptElem = findTestSubject(component, 'painlessScript').getDOMNode();
+
+ const failedShards = e.attributes?.failed_shards![0];
+ const script = failedShards!.reason.script;
+ expect(scriptElem.textContent).toBe(`Error executing Painless script: '${script}'`);
+
+ const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode();
+ const stackTrace = failedShards!.reason.script_stack!.join('\n');
+ expect(stackTraceElem.textContent).toBe(stackTrace);
+
+ expect(component.find('EuiButton').length).toBe(1);
+ });
+});
diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx
index 8a4248e48185b..6d11f3a16b09e 100644
--- a/src/plugins/data/public/search/errors/painless_error.tsx
+++ b/src/plugins/data/public/search/errors/painless_error.tsx
@@ -33,10 +33,12 @@ export class PainlessError extends EsError {
return (
<>
- {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', {
- defaultMessage: "Error executing Painless script: '{script}'.",
- values: { script: rootCause?.script },
- })}
+
+ {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', {
+ defaultMessage: "Error executing Painless script: '{script}'",
+ values: { script: rootCause?.script },
+ })}
+
{painlessStack ? (
diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx
index ee2703b888bf1..6b9ce1b422481 100644
--- a/src/plugins/data/public/search/errors/timeout_error.tsx
+++ b/src/plugins/data/public/search/errors/timeout_error.tsx
@@ -24,7 +24,7 @@ export enum TimeoutErrorMode {
*/
export class SearchTimeoutError extends KbnError {
public mode: TimeoutErrorMode;
- constructor(err: Error, mode: TimeoutErrorMode) {
+ constructor(err: Record, mode: TimeoutErrorMode) {
super(`Request timeout: ${JSON.stringify(err?.message)}`);
this.mode = mode;
}
diff --git a/src/plugins/data/public/search/errors/types.ts b/src/plugins/data/public/search/errors/types.ts
index d62cb311bf6a4..5806ef8676b9b 100644
--- a/src/plugins/data/public/search/errors/types.ts
+++ b/src/plugins/data/public/search/errors/types.ts
@@ -6,57 +6,47 @@
* Public License, v 1.
*/
+import { KibanaServerError } from '../../../../kibana_utils/common';
+
export interface FailedShard {
shard: number;
index: string;
node: string;
- reason: {
+ reason: Reason;
+}
+
+export interface Reason {
+ type: string;
+ reason: string;
+ script_stack?: string[];
+ position?: {
+ offset: number;
+ start: number;
+ end: number;
+ };
+ lang?: string;
+ script?: string;
+ caused_by?: {
type: string;
reason: string;
- script_stack: string[];
- script: string;
- lang: string;
- position: {
- offset: number;
- start: number;
- end: number;
- };
- caused_by: {
- type: string;
- reason: string;
- };
};
}
-export interface IEsError {
- body: {
- statusCode: number;
- error: string;
- message: string;
- attributes?: {
- error?: {
- root_cause?: [
- {
- lang: string;
- script: string;
- }
- ];
- type: string;
- reason: string;
- failed_shards: FailedShard[];
- caused_by: {
- type: string;
- reason: string;
- phase: string;
- grouped: boolean;
- failed_shards: FailedShard[];
- script_stack: string[];
- };
- };
- };
- };
+export interface IEsErrorAttributes {
+ type: string;
+ reason: string;
+ root_cause?: Reason[];
+ failed_shards?: FailedShard[];
}
+export type IEsError = KibanaServerError;
+
+/**
+ * Checks if a given errors originated from Elasticsearch.
+ * Those params are assigned to the attributes property of an error.
+ *
+ * @param e
+ */
export function isEsError(e: any): e is IEsError {
- return !!e.body?.attributes;
+ return !!e.attributes;
}
diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts
index d140e713f9440..7d303543a0c57 100644
--- a/src/plugins/data/public/search/errors/utils.ts
+++ b/src/plugins/data/public/search/errors/utils.ts
@@ -6,19 +6,15 @@
* Public License, v 1.
*/
-import { IEsError } from './types';
+import { FailedShard } from './types';
+import { KibanaServerError } from '../../../../kibana_utils/common';
-export function getFailedShards(err: IEsError) {
- const failedShards =
- err.body?.attributes?.error?.failed_shards ||
- err.body?.attributes?.error?.caused_by?.failed_shards;
+export function getFailedShards(err: KibanaServerError): FailedShard | undefined {
+ const errorInfo = err.attributes;
+ const failedShards = errorInfo?.failed_shards || errorInfo?.caused_by?.failed_shards;
return failedShards ? failedShards[0] : undefined;
}
-export function getTopLevelCause(err: IEsError) {
- return err.body?.attributes?.error;
-}
-
-export function getRootCause(err: IEsError) {
+export function getRootCause(err: KibanaServerError) {
return getFailedShards(err)?.reason;
}
diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts
index 5ae01eccdd920..bfd73951c31c4 100644
--- a/src/plugins/data/public/search/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor.test.ts
@@ -12,12 +12,15 @@ import { coreMock } from '../../../../core/public/mocks';
import { IEsSearchRequest } from '../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../../kibana_utils/public';
-import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors';
+import { SearchTimeoutError, PainlessError, TimeoutErrorMode, EsError } from './errors';
import { searchServiceMock } from './mocks';
import { ISearchStart, ISessionService } from '.';
import { bfetchPluginMock } from '../../../bfetch/public/mocks';
import { BfetchPublicSetup } from 'src/plugins/bfetch/public';
+import * as searchPhaseException from '../../common/search/test_data/search_phase_execution_exception.json';
+import * as resourceNotFoundException from '../../common/search/test_data/resource_not_found_exception.json';
+
let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys;
let bfetchSetup: jest.Mocked;
@@ -64,15 +67,9 @@ describe('SearchInterceptor', () => {
test('Renders a PainlessError', async () => {
searchInterceptor.showError(
new PainlessError({
- body: {
- attributes: {
- error: {
- failed_shards: {
- reason: 'bananas',
- },
- },
- },
- } as any,
+ statusCode: 400,
+ message: 'search_phase_execution_exception',
+ attributes: searchPhaseException.error,
})
);
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
@@ -161,10 +158,8 @@ describe('SearchInterceptor', () => {
describe('Should handle Timeout errors', () => {
test('Should throw SearchTimeoutError on server timeout AND show toast', async () => {
const mockResponse: any = {
- result: 500,
- body: {
- message: 'Request timed out',
- },
+ statusCode: 500,
+ message: 'Request timed out',
};
fetchMock.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
@@ -177,10 +172,8 @@ describe('SearchInterceptor', () => {
test('Timeout error should show multiple times if not in a session', async () => {
const mockResponse: any = {
- result: 500,
- body: {
- message: 'Request timed out',
- },
+ statusCode: 500,
+ message: 'Request timed out',
};
fetchMock.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
@@ -198,10 +191,8 @@ describe('SearchInterceptor', () => {
test('Timeout error should show once per each session', async () => {
const mockResponse: any = {
- result: 500,
- body: {
- message: 'Request timed out',
- },
+ statusCode: 500,
+ message: 'Request timed out',
};
fetchMock.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
@@ -219,10 +210,8 @@ describe('SearchInterceptor', () => {
test('Timeout error should show once in a single session', async () => {
const mockResponse: any = {
- result: 500,
- body: {
- message: 'Request timed out',
- },
+ statusCode: 500,
+ message: 'Request timed out',
};
fetchMock.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
@@ -240,22 +229,9 @@ describe('SearchInterceptor', () => {
test('Should throw Painless error on server error with OSS format', async () => {
const mockResponse: any = {
- result: 500,
- body: {
- attributes: {
- error: {
- failed_shards: [
- {
- reason: {
- lang: 'painless',
- script_stack: ['a', 'b'],
- reason: 'banana',
- },
- },
- ],
- },
- },
- },
+ statusCode: 400,
+ message: 'search_phase_execution_exception',
+ attributes: searchPhaseException.error,
};
fetchMock.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
@@ -265,6 +241,20 @@ describe('SearchInterceptor', () => {
await expect(response.toPromise()).rejects.toThrow(PainlessError);
});
+ test('Should throw ES error on ES server error', async () => {
+ const mockResponse: any = {
+ statusCode: 400,
+ message: 'resource_not_found_exception',
+ attributes: resourceNotFoundException.error,
+ };
+ fetchMock.mockRejectedValueOnce(mockResponse);
+ const mockRequest: IEsSearchRequest = {
+ params: {},
+ };
+ const response = searchInterceptor.search(mockRequest);
+ await expect(response.toPromise()).rejects.toThrow(EsError);
+ });
+
test('Observable should fail if user aborts (test merged signal)', async () => {
const abortController = new AbortController();
fetchMock.mockImplementationOnce((options: any) => {
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index f6ca9ef1a993d..6dfc8faea769e 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -6,7 +6,7 @@
* Public License, v 1.
*/
-import { get, memoize } from 'lodash';
+import { memoize } from 'lodash';
import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
@@ -25,7 +25,11 @@ import {
getHttpError,
} from './errors';
import { toMountPoint } from '../../../kibana_react/public';
-import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public';
+import {
+ AbortError,
+ getCombinedAbortSignal,
+ KibanaServerError,
+} from '../../../kibana_utils/public';
import { ISessionService } from './session';
export interface SearchInterceptorDeps {
@@ -87,8 +91,12 @@ export class SearchInterceptor {
* @returns `Error` a search service specific error or the original error, if a specific error can't be recognized.
* @internal
*/
- protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error {
- if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') {
+ protected handleSearchError(
+ e: KibanaServerError | AbortError,
+ timeoutSignal: AbortSignal,
+ options?: ISearchOptions
+ ): Error {
+ if (timeoutSignal.aborted || e.message === 'Request timed out') {
// Handle a client or a server side timeout
const err = new SearchTimeoutError(e, this.getTimeoutMode());
@@ -96,7 +104,7 @@ export class SearchInterceptor {
// The timeout error is shown any time a request times out, or once per session, if the request is part of a session.
this.showTimeoutError(err, options?.sessionId);
return err;
- } else if (options?.abortSignal?.aborted) {
+ } else if (e instanceof AbortError) {
// In the case an application initiated abort, throw the existing AbortError.
return e;
} else if (isEsError(e)) {
@@ -106,12 +114,13 @@ export class SearchInterceptor {
return new EsError(e);
}
} else {
- return e;
+ return e instanceof Error ? e : new Error(e.message);
}
}
/**
* @internal
+ * @throws `AbortError` | `ErrorLike`
*/
protected runSearch(
request: IKibanaSearchRequest,
@@ -234,7 +243,7 @@ export class SearchInterceptor {
});
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe(
- catchError((e: Error) => {
+ catchError((e: Error | AbortError) => {
return throwError(this.handleSearchError(e, timeoutSignal, options));
}),
finalize(() => {
diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
index 8e66729825e39..eeef46381732e 100644
--- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
+++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
@@ -6,37 +6,56 @@
* Public License, v 1.
*/
+import {
+ elasticsearchClientMock,
+ MockedTransportRequestPromise,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../../../core/server/elasticsearch/client/mocks';
import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks';
import { esSearchStrategyProvider } from './es_search_strategy';
import { SearchStrategyDependencies } from '../types';
+import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json';
+import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors';
+import { KbnServerError } from '../../../../kibana_utils/server';
+
describe('ES search strategy', () => {
+ const successBody = {
+ _shards: {
+ total: 10,
+ failed: 1,
+ skipped: 2,
+ successful: 7,
+ },
+ };
+ let mockedApiCaller: MockedTransportRequestPromise;
+ let mockApiCaller: jest.Mock<() => MockedTransportRequestPromise>;
const mockLogger: any = {
debug: () => {},
};
- const mockApiCaller = jest.fn().mockResolvedValue({
- body: {
- _shards: {
- total: 10,
- failed: 1,
- skipped: 2,
- successful: 7,
- },
- },
- });
- const mockDeps = ({
- uiSettingsClient: {
- get: () => {},
- },
- esClient: { asCurrentUser: { search: mockApiCaller } },
- } as unknown) as SearchStrategyDependencies;
+ function getMockedDeps(err?: Record) {
+ mockApiCaller = jest.fn().mockImplementation(() => {
+ if (err) {
+ mockedApiCaller = elasticsearchClientMock.createErrorTransportRequestPromise(err);
+ } else {
+ mockedApiCaller = elasticsearchClientMock.createSuccessTransportRequestPromise(
+ successBody,
+ { statusCode: 200 }
+ );
+ }
+ return mockedApiCaller;
+ });
- const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
+ return ({
+ uiSettingsClient: {
+ get: () => {},
+ },
+ esClient: { asCurrentUser: { search: mockApiCaller } },
+ } as unknown) as SearchStrategyDependencies;
+ }
- beforeEach(() => {
- mockApiCaller.mockClear();
- });
+ const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
it('returns a strategy with `search`', async () => {
const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger);
@@ -48,7 +67,7 @@ describe('ES search strategy', () => {
const params = { index: 'logstash-*' };
await esSearchStrategyProvider(mockConfig$, mockLogger)
- .search({ params }, {}, mockDeps)
+ .search({ params }, {}, getMockedDeps())
.subscribe(() => {
expect(mockApiCaller).toBeCalled();
expect(mockApiCaller.mock.calls[0][0]).toEqual({
@@ -64,7 +83,7 @@ describe('ES search strategy', () => {
const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
await esSearchStrategyProvider(mockConfig$, mockLogger)
- .search({ params }, {}, mockDeps)
+ .search({ params }, {}, getMockedDeps())
.subscribe(() => {
expect(mockApiCaller).toBeCalled();
expect(mockApiCaller.mock.calls[0][0]).toEqual({
@@ -82,13 +101,109 @@ describe('ES search strategy', () => {
params: { index: 'logstash-*' },
},
{},
- mockDeps
+ getMockedDeps()
)
.subscribe((data) => {
expect(data.isRunning).toBe(false);
expect(data.isPartial).toBe(false);
expect(data).toHaveProperty('loaded');
expect(data).toHaveProperty('rawResponse');
+ expect(mockedApiCaller.abort).not.toBeCalled();
done();
}));
+
+ it('can be aborted', async () => {
+ const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
+
+ const abortController = new AbortController();
+ abortController.abort();
+
+ await esSearchStrategyProvider(mockConfig$, mockLogger)
+ .search({ params }, { abortSignal: abortController.signal }, getMockedDeps())
+ .toPromise();
+
+ expect(mockApiCaller).toBeCalled();
+ expect(mockApiCaller.mock.calls[0][0]).toEqual({
+ ...params,
+ track_total_hits: true,
+ });
+ expect(mockedApiCaller.abort).toBeCalled();
+ });
+
+ it('throws normalized error if ResponseError is thrown', async (done) => {
+ const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
+ const errResponse = new ResponseError({
+ body: indexNotFoundException,
+ statusCode: 404,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ });
+
+ try {
+ await esSearchStrategyProvider(mockConfig$, mockLogger)
+ .search({ params }, {}, getMockedDeps(errResponse))
+ .toPromise();
+ } catch (e) {
+ expect(mockApiCaller).toBeCalled();
+ expect(e).toBeInstanceOf(KbnServerError);
+ expect(e.statusCode).toBe(404);
+ expect(e.message).toBe(errResponse.message);
+ expect(e.errBody).toBe(indexNotFoundException);
+ done();
+ }
+ });
+
+ it('throws normalized error if ElasticsearchClientError is thrown', async (done) => {
+ const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
+ const errResponse = new ElasticsearchClientError('This is a general ESClient error');
+
+ try {
+ await esSearchStrategyProvider(mockConfig$, mockLogger)
+ .search({ params }, {}, getMockedDeps(errResponse))
+ .toPromise();
+ } catch (e) {
+ expect(mockApiCaller).toBeCalled();
+ expect(e).toBeInstanceOf(KbnServerError);
+ expect(e.statusCode).toBe(500);
+ expect(e.message).toBe(errResponse.message);
+ expect(e.errBody).toBe(undefined);
+ done();
+ }
+ });
+
+ it('throws normalized error if ESClient throws unknown error', async (done) => {
+ const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
+ const errResponse = new Error('ESClient error');
+
+ try {
+ await esSearchStrategyProvider(mockConfig$, mockLogger)
+ .search({ params }, {}, getMockedDeps(errResponse))
+ .toPromise();
+ } catch (e) {
+ expect(mockApiCaller).toBeCalled();
+ expect(e).toBeInstanceOf(KbnServerError);
+ expect(e.statusCode).toBe(500);
+ expect(e.message).toBe(errResponse.message);
+ expect(e.errBody).toBe(undefined);
+ done();
+ }
+ });
+
+ it('throws KbnServerError for unknown index type', async (done) => {
+ const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' };
+
+ try {
+ await esSearchStrategyProvider(mockConfig$, mockLogger)
+ .search({ indexType: 'banana', params }, {}, getMockedDeps())
+ .toPromise();
+ } catch (e) {
+ expect(mockApiCaller).not.toBeCalled();
+ expect(e).toBeInstanceOf(KbnServerError);
+ expect(e.message).toBe('Unsupported index pattern type banana');
+ expect(e.statusCode).toBe(400);
+ expect(e.errBody).toBe(undefined);
+ done();
+ }
+ });
});
diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts
index a11bbe11f3f95..c176a50627b92 100644
--- a/src/plugins/data/server/search/es_search/es_search_strategy.ts
+++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts
@@ -15,13 +15,20 @@ import type { SearchUsage } from '../collectors';
import { getDefaultSearchParams, getShardTimeout, shimAbortSignal } from './request_utils';
import { toKibanaSearchResponse } from './response_utils';
import { searchUsageObserver } from '../collectors/usage';
-import { KbnServerError } from '../../../../kibana_utils/server';
+import { getKbnServerError, KbnServerError } from '../../../../kibana_utils/server';
export const esSearchStrategyProvider = (
config$: Observable,
logger: Logger,
usage?: SearchUsage
): ISearchStrategy => ({
+ /**
+ * @param request
+ * @param options
+ * @param deps
+ * @throws `KbnServerError`
+ * @returns `Observable>`
+ */
search: (request, { abortSignal }, { esClient, uiSettingsClient }) => {
// Only default index pattern type is supported here.
// See data_enhanced for other type support.
@@ -30,15 +37,19 @@ export const esSearchStrategyProvider = (
}
const search = async () => {
- const config = await config$.pipe(first()).toPromise();
- const params = {
- ...(await getDefaultSearchParams(uiSettingsClient)),
- ...getShardTimeout(config),
- ...request.params,
- };
- const promise = esClient.asCurrentUser.search>(params);
- const { body } = await shimAbortSignal(promise, abortSignal);
- return toKibanaSearchResponse(body);
+ try {
+ const config = await config$.pipe(first()).toPromise();
+ const params = {
+ ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...getShardTimeout(config),
+ ...request.params,
+ };
+ const promise = esClient.asCurrentUser.search>(params);
+ const { body } = await shimAbortSignal(promise, abortSignal);
+ return toKibanaSearchResponse(body);
+ } catch (e) {
+ throw getKbnServerError(e);
+ }
};
return from(search()).pipe(tap(searchUsageObserver(logger, usage)));
diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts
new file mode 100644
index 0000000000000..e30b7bdaa8402
--- /dev/null
+++ b/src/plugins/data/server/search/routes/bsearch.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { catchError, first, map } from 'rxjs/operators';
+import { CoreStart, KibanaRequest } from 'src/core/server';
+import { BfetchServerSetup } from 'src/plugins/bfetch/server';
+import {
+ IKibanaSearchRequest,
+ IKibanaSearchResponse,
+ ISearchClient,
+ ISearchOptions,
+} from '../../../common/search';
+import { shimHitsTotal } from './shim_hits_total';
+
+type GetScopedProider = (coreStart: CoreStart) => (request: KibanaRequest) => ISearchClient;
+
+export function registerBsearchRoute(
+ bfetch: BfetchServerSetup,
+ coreStartPromise: Promise<[CoreStart, {}, {}]>,
+ getScopedProvider: GetScopedProider
+): void {
+ bfetch.addBatchProcessingRoute<
+ { request: IKibanaSearchRequest; options?: ISearchOptions },
+ IKibanaSearchResponse
+ >('/internal/bsearch', (request) => {
+ return {
+ /**
+ * @param requestOptions
+ * @throws `KibanaServerError`
+ */
+ onBatchItem: async ({ request: requestData, options }) => {
+ const coreStart = await coreStartPromise;
+ const search = getScopedProvider(coreStart[0])(request);
+ return search
+ .search(requestData, options)
+ .pipe(
+ first(),
+ map((response) => {
+ return {
+ ...response,
+ ...{
+ rawResponse: shimHitsTotal(response.rawResponse),
+ },
+ };
+ }),
+ catchError((err) => {
+ // Re-throw as object, to get attributes passed to the client
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ message: err.message,
+ statusCode: err.statusCode,
+ attributes: err.errBody?.error,
+ };
+ })
+ )
+ .toPromise();
+ },
+ };
+ });
+}
diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts
index 6578774f65a3c..fc30e2f29c3ef 100644
--- a/src/plugins/data/server/search/routes/call_msearch.ts
+++ b/src/plugins/data/server/search/routes/call_msearch.ts
@@ -8,12 +8,12 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
-import { ApiResponse } from '@elastic/elasticsearch';
import { SearchResponse } from 'elasticsearch';
import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server';
import type { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source';
import { shimHitsTotal } from './shim_hits_total';
+import { getKbnServerError } from '../../../../kibana_utils/server';
import { getShardTimeout, getDefaultSearchParams, shimAbortSignal } from '..';
/** @internal */
@@ -48,6 +48,9 @@ interface CallMsearchDependencies {
* @internal
*/
export function getCallMsearch(dependencies: CallMsearchDependencies) {
+ /**
+ * @throws KbnServerError
+ */
return async (params: {
body: MsearchRequestBody;
signal?: AbortSignal;
@@ -61,28 +64,29 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) {
// trackTotalHits is not supported by msearch
const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(uiSettings);
- const body = convertRequestBody(params.body, timeout);
-
- const promise = shimAbortSignal(
- esClient.asCurrentUser.msearch(
+ try {
+ const promise = esClient.asCurrentUser.msearch(
{
- body,
+ body: convertRequestBody(params.body, timeout),
},
{
querystring: defaultParams,
}
- ),
- params.signal
- );
- const response = (await promise) as ApiResponse<{ responses: Array> }>;
+ );
+ const response = await shimAbortSignal(promise, params.signal);
- return {
- body: {
- ...response,
+ return {
body: {
- responses: response.body.responses?.map((r: SearchResponse) => shimHitsTotal(r)),
+ ...response,
+ body: {
+ responses: response.body.responses?.map((r: SearchResponse) =>
+ shimHitsTotal(r)
+ ),
+ },
},
- },
- };
+ };
+ } catch (e) {
+ throw getKbnServerError(e);
+ }
};
}
diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts
index 02f200d5435dd..a847931a49123 100644
--- a/src/plugins/data/server/search/routes/msearch.test.ts
+++ b/src/plugins/data/server/search/routes/msearch.test.ts
@@ -24,6 +24,8 @@ import { convertRequestBody } from './call_msearch';
import { registerMsearchRoute } from './msearch';
import { DataPluginStart } from '../../plugin';
import { dataPluginMock } from '../../mocks';
+import * as jsonEofException from '../../../common/search/test_data/json_e_o_f_exception.json';
+import { ResponseError } from '@elastic/elasticsearch/lib/errors';
describe('msearch route', () => {
let mockDataStart: MockedKeys;
@@ -76,15 +78,52 @@ describe('msearch route', () => {
});
});
- it('handler throws an error if the search throws an error', async () => {
- const response = {
- message: 'oh no',
- body: {
- error: 'oops',
+ it('handler returns an error response if the search throws an error', async () => {
+ const rejectedValue = Promise.reject(
+ new ResponseError({
+ body: jsonEofException,
+ statusCode: 400,
+ meta: {} as any,
+ headers: [],
+ warnings: [],
+ })
+ );
+ const mockClient = {
+ msearch: jest.fn().mockReturnValue(rejectedValue),
+ };
+ const mockContext = {
+ core: {
+ elasticsearch: { client: { asCurrentUser: mockClient } },
+ uiSettings: { client: { get: jest.fn() } },
},
};
+ const mockBody = { searches: [{ header: {}, body: {} }] };
+ const mockQuery = {};
+ const mockRequest = httpServerMock.createKibanaRequest({
+ body: mockBody,
+ query: mockQuery,
+ });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ });
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const handler = mockRouter.post.mock.calls[0][1];
+ await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
+
+ expect(mockClient.msearch).toBeCalledTimes(1);
+ expect(mockResponse.customError).toBeCalled();
+
+ const error: any = mockResponse.customError.mock.calls[0][0];
+ expect(error.statusCode).toBe(400);
+ expect(error.body.message).toBe('json_e_o_f_exception');
+ expect(error.body.attributes).toBe(jsonEofException.error);
+ });
+
+ it('handler returns an error response if the search throws a general error', async () => {
+ const rejectedValue = Promise.reject(new Error('What happened?'));
const mockClient = {
- msearch: jest.fn().mockReturnValue(Promise.reject(response)),
+ msearch: jest.fn().mockReturnValue(rejectedValue),
};
const mockContext = {
core: {
@@ -106,11 +145,12 @@ describe('msearch route', () => {
const handler = mockRouter.post.mock.calls[0][1];
await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
- expect(mockClient.msearch).toBeCalled();
+ expect(mockClient.msearch).toBeCalledTimes(1);
expect(mockResponse.customError).toBeCalled();
const error: any = mockResponse.customError.mock.calls[0][0];
- expect(error.body.message).toBe('oh no');
- expect(error.body.attributes.error).toBe('oops');
+ expect(error.statusCode).toBe(500);
+ expect(error.body.message).toBe('What happened?');
+ expect(error.body.attributes).toBe(undefined);
});
});
diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts
index f47a42cf9d82b..2cde6d19e4c18 100644
--- a/src/plugins/data/server/search/routes/search.test.ts
+++ b/src/plugins/data/server/search/routes/search.test.ts
@@ -12,11 +12,27 @@ import { CoreSetup, RequestHandlerContext } from 'src/core/server';
import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { registerSearchRoute } from './search';
import { DataPluginStart } from '../../plugin';
+import * as searchPhaseException from '../../../common/search/test_data/search_phase_execution_exception.json';
+import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json';
+import { KbnServerError } from '../../../../kibana_utils/server';
describe('Search service', () => {
let mockCoreSetup: MockedKeys>;
+ function mockEsError(message: string, statusCode: number, attributes?: Record) {
+ return new KbnServerError(message, statusCode, attributes);
+ }
+
+ async function runMockSearch(mockContext: any, mockRequest: any, mockResponse: any) {
+ registerSearchRoute(mockCoreSetup.http.createRouter());
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const handler = mockRouter.post.mock.calls[0][1];
+ await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
+ }
+
beforeEach(() => {
+ jest.clearAllMocks();
mockCoreSetup = coreMock.createSetup();
});
@@ -54,11 +70,7 @@ describe('Search service', () => {
});
const mockResponse = httpServerMock.createResponseFactory();
- registerSearchRoute(mockCoreSetup.http.createRouter());
-
- const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const handler = mockRouter.post.mock.calls[0][1];
- await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
+ await runMockSearch(mockContext, mockRequest, mockResponse);
expect(mockContext.search.search).toBeCalled();
expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody);
@@ -68,14 +80,9 @@ describe('Search service', () => {
});
});
- it('handler throws an error if the search throws an error', async () => {
+ it('handler returns an error response if the search throws a painless error', async () => {
const rejectedValue = from(
- Promise.reject({
- message: 'oh no',
- body: {
- error: 'oops',
- },
- })
+ Promise.reject(mockEsError('search_phase_execution_exception', 400, searchPhaseException))
);
const mockContext = {
@@ -84,25 +91,69 @@ describe('Search service', () => {
},
};
- const mockBody = { id: undefined, params: {} };
- const mockParams = { strategy: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
- body: mockBody,
- params: mockParams,
+ body: { id: undefined, params: {} },
+ params: { strategy: 'foo' },
});
const mockResponse = httpServerMock.createResponseFactory();
- registerSearchRoute(mockCoreSetup.http.createRouter());
+ await runMockSearch(mockContext, mockRequest, mockResponse);
- const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const handler = mockRouter.post.mock.calls[0][1];
- await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
+ // verify error
+ expect(mockResponse.customError).toBeCalled();
+ const error: any = mockResponse.customError.mock.calls[0][0];
+ expect(error.statusCode).toBe(400);
+ expect(error.body.message).toBe('search_phase_execution_exception');
+ expect(error.body.attributes).toBe(searchPhaseException.error);
+ });
+
+ it('handler returns an error response if the search throws an index not found error', async () => {
+ const rejectedValue = from(
+ Promise.reject(mockEsError('index_not_found_exception', 404, indexNotFoundException))
+ );
+
+ const mockContext = {
+ search: {
+ search: jest.fn().mockReturnValue(rejectedValue),
+ },
+ };
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ body: { id: undefined, params: {} },
+ params: { strategy: 'foo' },
+ });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ await runMockSearch(mockContext, mockRequest, mockResponse);
+
+ expect(mockResponse.customError).toBeCalled();
+ const error: any = mockResponse.customError.mock.calls[0][0];
+ expect(error.statusCode).toBe(404);
+ expect(error.body.message).toBe('index_not_found_exception');
+ expect(error.body.attributes).toBe(indexNotFoundException.error);
+ });
+
+ it('handler returns an error response if the search throws a general error', async () => {
+ const rejectedValue = from(Promise.reject(new Error('This is odd')));
+
+ const mockContext = {
+ search: {
+ search: jest.fn().mockReturnValue(rejectedValue),
+ },
+ };
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ body: { id: undefined, params: {} },
+ params: { strategy: 'foo' },
+ });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ await runMockSearch(mockContext, mockRequest, mockResponse);
- expect(mockContext.search.search).toBeCalled();
- expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody);
expect(mockResponse.customError).toBeCalled();
const error: any = mockResponse.customError.mock.calls[0][0];
- expect(error.body.message).toBe('oh no');
- expect(error.body.attributes.error).toBe('oops');
+ expect(error.statusCode).toBe(500);
+ expect(error.body.message).toBe('This is odd');
+ expect(error.body.attributes).toBe(undefined);
});
});
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index f1a6fc09ee21f..63593bbe84a08 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -6,7 +6,7 @@
* Public License, v 1.
*/
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { pick } from 'lodash';
import {
CoreSetup,
@@ -18,7 +18,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
-import { catchError, first, map } from 'rxjs/operators';
+import { first } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
@@ -64,6 +64,7 @@ import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
import { ConfigSchema } from '../../config';
import { SessionService, IScopedSessionService, ISessionService } from './session';
import { KbnServerError } from '../../../kibana_utils/server';
+import { registerBsearchRoute } from './routes/bsearch';
type StrategyMap = Record>;
@@ -137,43 +138,7 @@ export class SearchService implements Plugin {
)
);
- bfetch.addBatchProcessingRoute<
- { request: IKibanaSearchResponse; options?: ISearchOptions },
- any
- >('/internal/bsearch', (request) => {
- const search = this.asScopedProvider(this.coreStart!)(request);
-
- return {
- onBatchItem: async ({ request: requestData, options }) => {
- return search
- .search(requestData, options)
- .pipe(
- first(),
- map((response) => {
- return {
- ...response,
- ...{
- rawResponse: shimHitsTotal(response.rawResponse),
- },
- };
- }),
- catchError((err) => {
- // eslint-disable-next-line no-throw-literal
- throw {
- statusCode: err.statusCode || 500,
- body: {
- message: err.message,
- attributes: {
- error: err.body?.error || err.message,
- },
- },
- };
- })
- )
- .toPromise();
- },
- };
- });
+ registerBsearchRoute(bfetch, core.getStartServices(), this.asScopedProvider);
core.savedObjects.registerType(searchTelemetry);
if (usageCollection) {
@@ -285,10 +250,14 @@ export class SearchService implements Plugin {
options: ISearchOptions,
deps: SearchStrategyDependencies
) => {
- const strategy = this.getSearchStrategy(
- options.strategy
- );
- return session.search(strategy, request, options, deps);
+ try {
+ const strategy = this.getSearchStrategy(
+ options.strategy
+ );
+ return session.search(strategy, request, options, deps);
+ } catch (e) {
+ return throwError(e);
+ }
};
private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => {
diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json
index 81bcb3b02e100..21560b1328840 100644
--- a/src/plugins/data/tsconfig.json
+++ b/src/plugins/data/tsconfig.json
@@ -7,7 +7,7 @@
"declaration": true,
"declarationMap": true
},
- "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"],
+ "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts", "common/**/*.json"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../bfetch/tsconfig.json" },
diff --git a/src/plugins/kibana_utils/common/errors/index.ts b/src/plugins/kibana_utils/common/errors/index.ts
index 354cf1d504b28..f859e0728269a 100644
--- a/src/plugins/kibana_utils/common/errors/index.ts
+++ b/src/plugins/kibana_utils/common/errors/index.ts
@@ -7,3 +7,4 @@
*/
export * from './errors';
+export * from './types';
diff --git a/src/plugins/kibana_utils/common/errors/types.ts b/src/plugins/kibana_utils/common/errors/types.ts
new file mode 100644
index 0000000000000..89e83586dc115
--- /dev/null
+++ b/src/plugins/kibana_utils/common/errors/types.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+export interface KibanaServerError {
+ statusCode: number;
+ message: string;
+ attributes?: T;
+}
diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts
index f95ffe5c3d7b6..821118ea4640d 100644
--- a/src/plugins/kibana_utils/server/index.ts
+++ b/src/plugins/kibana_utils/server/index.ts
@@ -18,4 +18,4 @@ export {
url,
} from '../common';
-export { KbnServerError, reportServerError } from './report_server_error';
+export { KbnServerError, reportServerError, getKbnServerError } from './report_server_error';
diff --git a/src/plugins/kibana_utils/server/report_server_error.ts b/src/plugins/kibana_utils/server/report_server_error.ts
index 664f34ca7ad51..01e80cfc7184d 100644
--- a/src/plugins/kibana_utils/server/report_server_error.ts
+++ b/src/plugins/kibana_utils/server/report_server_error.ts
@@ -6,23 +6,42 @@
* Public License, v 1.
*/
+import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { KibanaResponseFactory } from 'kibana/server';
import { KbnError } from '../common';
export class KbnServerError extends KbnError {
- constructor(message: string, public readonly statusCode: number) {
+ public errBody?: Record;
+ constructor(message: string, public readonly statusCode: number, errBody?: Record) {
super(message);
+ this.errBody = errBody;
}
}
-export function reportServerError(res: KibanaResponseFactory, err: any) {
+/**
+ * Formats any error thrown into a standardized `KbnServerError`.
+ * @param e `Error` or `ElasticsearchClientError`
+ * @returns `KbnServerError`
+ */
+export function getKbnServerError(e: Error) {
+ return new KbnServerError(
+ e.message ?? 'Unknown error',
+ e instanceof ResponseError ? e.statusCode : 500,
+ e instanceof ResponseError ? e.body : undefined
+ );
+}
+
+/**
+ *
+ * @param res Formats a `KbnServerError` into a server error response
+ * @param err
+ */
+export function reportServerError(res: KibanaResponseFactory, err: KbnServerError) {
return res.customError({
statusCode: err.statusCode ?? 500,
body: {
message: err.message,
- attributes: {
- error: err.body?.error || err.message,
- },
+ attributes: err.errBody?.error,
},
});
}
diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts
new file mode 100644
index 0000000000000..504680d28bf83
--- /dev/null
+++ b/test/api_integration/apis/search/bsearch.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import request from 'superagent';
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { painlessErrReq } from './painless_err_req';
+import { verifyErrorResponse } from './verify_error';
+
+function parseBfetchResponse(resp: request.Response): Array> {
+ return resp.text
+ .trim()
+ .split('\n')
+ .map((item) => JSON.parse(item));
+}
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ describe('bsearch', () => {
+ describe('post', () => {
+ it('should return 200 a single response', async () => {
+ const resp = await supertest.post(`/internal/bsearch`).send({
+ batch: [
+ {
+ request: {
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ const jsonBody = JSON.parse(resp.text);
+
+ expect(resp.status).to.be(200);
+ expect(jsonBody.id).to.be(0);
+ expect(jsonBody.result.isPartial).to.be(false);
+ expect(jsonBody.result.isRunning).to.be(false);
+ expect(jsonBody.result).to.have.property('rawResponse');
+ });
+
+ it('should return a batch of successful resposes', async () => {
+ const resp = await supertest.post(`/internal/bsearch`).send({
+ batch: [
+ {
+ request: {
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ },
+ },
+ {
+ request: {
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(resp.status).to.be(200);
+ const parsedResponse = parseBfetchResponse(resp);
+ expect(parsedResponse).to.have.length(2);
+ parsedResponse.forEach((responseJson) => {
+ expect(responseJson.result.isPartial).to.be(false);
+ expect(responseJson.result.isRunning).to.be(false);
+ expect(responseJson.result).to.have.property('rawResponse');
+ });
+ });
+
+ it('should return error for not found strategy', async () => {
+ const resp = await supertest.post(`/internal/bsearch`).send({
+ batch: [
+ {
+ request: {
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ },
+ options: {
+ strategy: 'wtf',
+ },
+ },
+ ],
+ });
+
+ expect(resp.status).to.be(200);
+ parseBfetchResponse(resp).forEach((responseJson, i) => {
+ expect(responseJson.id).to.be(i);
+ verifyErrorResponse(responseJson.error, 404, 'Search strategy wtf not found');
+ });
+ });
+
+ it('should return 400 when index type is provided in OSS', async () => {
+ const resp = await supertest.post(`/internal/bsearch`).send({
+ batch: [
+ {
+ request: {
+ indexType: 'baad',
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(resp.status).to.be(200);
+ parseBfetchResponse(resp).forEach((responseJson, i) => {
+ expect(responseJson.id).to.be(i);
+ verifyErrorResponse(responseJson.error, 400, 'Unsupported index pattern type baad');
+ });
+ });
+
+ describe('painless', () => {
+ before(async () => {
+ await esArchiver.loadIfNeeded(
+ '../../../functional/fixtures/es_archiver/logstash_functional'
+ );
+ });
+
+ after(async () => {
+ await esArchiver.unload('../../../functional/fixtures/es_archiver/logstash_functional');
+ });
+ it('should return 400 for Painless error', async () => {
+ const resp = await supertest.post(`/internal/bsearch`).send({
+ batch: [
+ {
+ request: painlessErrReq,
+ },
+ ],
+ });
+
+ expect(resp.status).to.be(200);
+ parseBfetchResponse(resp).forEach((responseJson, i) => {
+ expect(responseJson.id).to.be(i);
+ verifyErrorResponse(responseJson.error, 400, 'search_phase_execution_exception', true);
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/test/api_integration/apis/search/index.ts b/test/api_integration/apis/search/index.ts
index 2f21825d6902f..6e90bf0f22c51 100644
--- a/test/api_integration/apis/search/index.ts
+++ b/test/api_integration/apis/search/index.ts
@@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('search', () => {
loadTestFile(require.resolve('./search'));
+ loadTestFile(require.resolve('./bsearch'));
loadTestFile(require.resolve('./msearch'));
});
}
diff --git a/test/api_integration/apis/search/painless_err_req.ts b/test/api_integration/apis/search/painless_err_req.ts
new file mode 100644
index 0000000000000..6fbf6565d7a9e
--- /dev/null
+++ b/test/api_integration/apis/search/painless_err_req.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+export const painlessErrReq = {
+ params: {
+ index: 'log*',
+ body: {
+ size: 500,
+ fields: ['*'],
+ script_fields: {
+ invalid_scripted_field: {
+ script: {
+ source: 'invalid',
+ lang: 'painless',
+ },
+ },
+ },
+ stored_fields: ['*'],
+ query: {
+ bool: {
+ filter: [
+ {
+ match_all: {},
+ },
+ {
+ range: {
+ '@timestamp': {
+ gte: '2015-01-19T12:27:55.047Z',
+ lte: '2021-01-19T12:27:55.047Z',
+ format: 'strict_date_optional_time',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts
index fc13189a40753..155705f81fa8a 100644
--- a/test/api_integration/apis/search/search.ts
+++ b/test/api_integration/apis/search/search.ts
@@ -8,11 +8,21 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
+import { painlessErrReq } from './painless_err_req';
+import { verifyErrorResponse } from './verify_error';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
describe('search', () => {
+ before(async () => {
+ await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional');
+ });
+
+ after(async () => {
+ await esArchiver.unload('../../../functional/fixtures/es_archiver/logstash_functional');
+ });
describe('post', () => {
it('should return 200 when correctly formatted searches are provided', async () => {
const resp = await supertest
@@ -28,13 +38,37 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(200);
+ expect(resp.status).to.be(200);
expect(resp.body.isPartial).to.be(false);
expect(resp.body.isRunning).to.be(false);
expect(resp.body).to.have.property('rawResponse');
});
- it('should return 404 when if no strategy is provided', async () =>
- await supertest
+ it('should return 200 if terminated early', async () => {
+ const resp = await supertest
+ .post(`/internal/search/es`)
+ .send({
+ params: {
+ terminateAfter: 1,
+ index: 'log*',
+ size: 1000,
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ })
+ .expect(200);
+
+ expect(resp.status).to.be(200);
+ expect(resp.body.isPartial).to.be(false);
+ expect(resp.body.isRunning).to.be(false);
+ expect(resp.body.rawResponse.terminated_early).to.be(true);
+ });
+
+ it('should return 404 when if no strategy is provided', async () => {
+ const resp = await supertest
.post(`/internal/search`)
.send({
body: {
@@ -43,7 +77,10 @@ export default function ({ getService }: FtrProviderContext) {
},
},
})
- .expect(404));
+ .expect(404);
+
+ verifyErrorResponse(resp.body, 404);
+ });
it('should return 404 when if unknown strategy is provided', async () => {
const resp = await supertest
@@ -56,6 +93,8 @@ export default function ({ getService }: FtrProviderContext) {
},
})
.expect(404);
+
+ verifyErrorResponse(resp.body, 404);
expect(resp.body.message).to.contain('banana not found');
});
@@ -74,11 +113,33 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(400);
+ verifyErrorResponse(resp.body, 400);
+
expect(resp.body.message).to.contain('Unsupported index pattern');
});
+ it('should return 400 with illegal ES argument', async () => {
+ const resp = await supertest
+ .post(`/internal/search/es`)
+ .send({
+ params: {
+ timeout: 1, // This should be a time range string!
+ index: 'log*',
+ size: 1000,
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ })
+ .expect(400);
+
+ verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true);
+ });
+
it('should return 400 with a bad body', async () => {
- await supertest
+ const resp = await supertest
.post(`/internal/search/es`)
.send({
params: {
@@ -89,16 +150,26 @@ export default function ({ getService }: FtrProviderContext) {
},
})
.expect(400);
+
+ verifyErrorResponse(resp.body, 400, 'parsing_exception', true);
+ });
+
+ it('should return 400 for a painless error', async () => {
+ const resp = await supertest.post(`/internal/search/es`).send(painlessErrReq).expect(400);
+
+ verifyErrorResponse(resp.body, 400, 'search_phase_execution_exception', true);
});
});
describe('delete', () => {
it('should return 404 when no search id provided', async () => {
- await supertest.delete(`/internal/search/es`).send().expect(404);
+ const resp = await supertest.delete(`/internal/search/es`).send().expect(404);
+ verifyErrorResponse(resp.body, 404);
});
it('should return 400 when trying a delete on a non supporting strategy', async () => {
const resp = await supertest.delete(`/internal/search/es/123`).send().expect(400);
+ verifyErrorResponse(resp.body, 400);
expect(resp.body.message).to.contain("Search strategy es doesn't support cancellations");
});
});
diff --git a/test/api_integration/apis/search/verify_error.ts b/test/api_integration/apis/search/verify_error.ts
new file mode 100644
index 0000000000000..a5754ff47973e
--- /dev/null
+++ b/test/api_integration/apis/search/verify_error.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+
+export const verifyErrorResponse = (
+ r: any,
+ expectedCode: number,
+ message?: string,
+ shouldHaveAttrs?: boolean
+) => {
+ expect(r.statusCode).to.be(expectedCode);
+ if (message) {
+ expect(r.message).to.be(message);
+ }
+ if (shouldHaveAttrs) {
+ expect(r).to.have.property('attributes');
+ expect(r.attributes).to.have.property('root_cause');
+ } else {
+ expect(r).not.to.have.property('attributes');
+ }
+};
diff --git a/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json b/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json
new file mode 100644
index 0000000000000..b79a396445e3d
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json
@@ -0,0 +1,229 @@
+{
+ "error": {
+ "root_cause": [
+ {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ }
+ },
+ {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ }
+ },
+ {
+ "type": "parse_exception",
+ "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]: [failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]]"
+ },
+ {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ }
+ },
+ {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ }
+ },
+ {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ }
+ }
+ ],
+ "type": "search_phase_execution_exception",
+ "reason": "all shards failed",
+ "phase": "query",
+ "grouped": true,
+ "failed_shards": [
+ {
+ "shard": 0,
+ "index": ".apm-agent-configuration",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ },
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "cannot resolve symbol [invalid]"
+ }
+ }
+ },
+ {
+ "shard": 0,
+ "index": ".apm-custom-link",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ },
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "cannot resolve symbol [invalid]"
+ }
+ }
+ },
+ {
+ "shard": 0,
+ "index": ".kibana-event-log-8.0.0-000001",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "parse_exception",
+ "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]: [failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]]",
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]",
+ "caused_by": {
+ "type": "date_time_parse_exception",
+ "reason": "Text '2021-01-19T12:2755.047Z' could not be parsed, unparsed text found at index 16"
+ }
+ }
+ }
+ },
+ {
+ "shard": 0,
+ "index": ".kibana_1",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ },
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "cannot resolve symbol [invalid]"
+ }
+ }
+ },
+ {
+ "shard": 0,
+ "index": ".kibana_task_manager_1",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ },
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "cannot resolve symbol [invalid]"
+ }
+ }
+ },
+ {
+ "shard": 0,
+ "index": ".security-7",
+ "node": "DEfMVCg5R12TRG4CYIxUgQ",
+ "reason": {
+ "type": "script_exception",
+ "reason": "compile error",
+ "script_stack": [
+ "invalid",
+ "^---- HERE"
+ ],
+ "script": "invalid",
+ "lang": "painless",
+ "position": {
+ "offset": 0,
+ "start": 0,
+ "end": 7
+ },
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "cannot resolve symbol [invalid]"
+ }
+ }
+ }
+ ]
+ },
+ "status": 400
+}
\ No newline at end of file
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
index 1a6fc724e2cf2..22b0f3272ff7d 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
@@ -9,10 +9,16 @@ import { EnhancedSearchInterceptor } from './search_interceptor';
import { CoreSetup, CoreStart } from 'kibana/public';
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { AbortError } from '../../../../../src/plugins/kibana_utils/public';
-import { ISessionService, SearchTimeoutError, SearchSessionState } from 'src/plugins/data/public';
+import {
+ ISessionService,
+ SearchTimeoutError,
+ SearchSessionState,
+ PainlessError,
+} from 'src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks';
import { BehaviorSubject } from 'rxjs';
+import * as xpackResourceNotFoundException from '../../common/search/test_data/search_phase_execution_exception.json';
const timeTravel = (msToRun = 0) => {
jest.advanceTimersByTime(msToRun);
@@ -99,6 +105,33 @@ describe('EnhancedSearchInterceptor', () => {
});
});
+ describe('errors', () => {
+ test('Should throw Painless error on server error with OSS format', async () => {
+ const mockResponse: any = {
+ statusCode: 400,
+ message: 'search_phase_execution_exception',
+ attributes: xpackResourceNotFoundException.error,
+ };
+ fetchMock.mockRejectedValueOnce(mockResponse);
+ const response = searchInterceptor.search({
+ params: {},
+ });
+ await expect(response.toPromise()).rejects.toThrow(PainlessError);
+ });
+
+ test('Renders a PainlessError', async () => {
+ searchInterceptor.showError(
+ new PainlessError({
+ statusCode: 400,
+ message: 'search_phase_execution_exception',
+ attributes: xpackResourceNotFoundException.error,
+ })
+ );
+ expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
+ expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled();
+ });
+ });
+
describe('search', () => {
test('should resolve immediately if first call returns full result', async () => {
const responses = [
@@ -342,7 +375,8 @@ describe('EnhancedSearchInterceptor', () => {
{
time: 10,
value: {
- error: 'oh no',
+ statusCode: 500,
+ message: 'oh no',
id: 1,
},
isError: true,
@@ -364,7 +398,8 @@ describe('EnhancedSearchInterceptor', () => {
await timeTravel(10);
expect(error).toHaveBeenCalled();
- expect(error.mock.calls[0][0]).toBe(responses[1].value);
+ expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
+ expect((error.mock.calls[0][0] as Error).message).toBe('oh no');
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(mockCoreSetup.http.delete).toHaveBeenCalledTimes(1);
});
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index 3230895da7705..b2ddd0310f8f5 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -7,6 +7,10 @@
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
import { BehaviorSubject } from 'rxjs';
import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server/search';
+import { KbnServerError } from '../../../../../src/plugins/kibana_utils/server';
+import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors';
+import * as indexNotFoundException from '../../../../../src/plugins/data/common/search/test_data/index_not_found_exception.json';
+import * as xContentParseException from '../../../../../src/plugins/data/common/search/test_data/x_content_parse_exception.json';
const mockAsyncResponse = {
body: {
@@ -145,6 +149,54 @@ describe('ES search strategy', () => {
expect(request).toHaveProperty('wait_for_completion_timeout');
expect(request).toHaveProperty('keep_alive');
});
+
+ it('throws normalized error if ResponseError is thrown', async () => {
+ const errResponse = new ResponseError({
+ body: indexNotFoundException,
+ statusCode: 404,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ });
+
+ mockSubmitCaller.mockRejectedValue(errResponse);
+
+ const params = { index: 'logstash-*', body: { query: {} } };
+ const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+
+ let err: KbnServerError | undefined;
+ try {
+ await esSearch.search({ params }, {}, mockDeps).toPromise();
+ } catch (e) {
+ err = e;
+ }
+ expect(mockSubmitCaller).toBeCalled();
+ expect(err).toBeInstanceOf(KbnServerError);
+ expect(err?.statusCode).toBe(404);
+ expect(err?.message).toBe(errResponse.message);
+ expect(err?.errBody).toBe(indexNotFoundException);
+ });
+
+ it('throws normalized error if Error is thrown', async () => {
+ const errResponse = new Error('not good');
+
+ mockSubmitCaller.mockRejectedValue(errResponse);
+
+ const params = { index: 'logstash-*', body: { query: {} } };
+ const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+
+ let err: KbnServerError | undefined;
+ try {
+ await esSearch.search({ params }, {}, mockDeps).toPromise();
+ } catch (e) {
+ err = e;
+ }
+ expect(mockSubmitCaller).toBeCalled();
+ expect(err).toBeInstanceOf(KbnServerError);
+ expect(err?.statusCode).toBe(500);
+ expect(err?.message).toBe(errResponse.message);
+ expect(err?.errBody).toBe(undefined);
+ });
});
describe('cancel', () => {
@@ -160,6 +212,33 @@ describe('ES search strategy', () => {
const request = mockDeleteCaller.mock.calls[0][0];
expect(request).toEqual({ id });
});
+
+ it('throws normalized error on ResponseError', async () => {
+ const errResponse = new ResponseError({
+ body: xContentParseException,
+ statusCode: 400,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ });
+ mockDeleteCaller.mockRejectedValue(errResponse);
+
+ const id = 'some_id';
+ const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+
+ let err: KbnServerError | undefined;
+ try {
+ await esSearch.cancel!(id, {}, mockDeps);
+ } catch (e) {
+ err = e;
+ }
+
+ expect(mockDeleteCaller).toBeCalled();
+ expect(err).toBeInstanceOf(KbnServerError);
+ expect(err?.statusCode).toBe(400);
+ expect(err?.message).toBe(errResponse.message);
+ expect(err?.errBody).toBe(xContentParseException);
+ });
});
describe('extend', () => {
@@ -176,5 +255,27 @@ describe('ES search strategy', () => {
const request = mockGetCaller.mock.calls[0][0];
expect(request).toEqual({ id, keep_alive: keepAlive });
});
+
+ it('throws normalized error on ElasticsearchClientError', async () => {
+ const errResponse = new ElasticsearchClientError('something is wrong with EsClient');
+ mockGetCaller.mockRejectedValue(errResponse);
+
+ const id = 'some_other_id';
+ const keepAlive = '1d';
+ const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+
+ let err: KbnServerError | undefined;
+ try {
+ await esSearch.extend!(id, keepAlive, {}, mockDeps);
+ } catch (e) {
+ err = e;
+ }
+
+ expect(mockGetCaller).toBeCalled();
+ expect(err).toBeInstanceOf(KbnServerError);
+ expect(err?.statusCode).toBe(500);
+ expect(err?.message).toBe(errResponse.message);
+ expect(err?.errBody).toBe(undefined);
+ });
});
});
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 54ed59b30952a..694d9807b5a4d 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -6,7 +6,7 @@
import type { Observable } from 'rxjs';
import type { IScopedClusterClient, Logger, SharedGlobalConfig } from 'kibana/server';
-import { first, tap } from 'rxjs/operators';
+import { catchError, first, tap } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
import { from } from 'rxjs';
import type {
@@ -33,7 +33,7 @@ import {
} from './request_utils';
import { toAsyncKibanaSearchResponse } from './response_utils';
import { AsyncSearchResponse } from './types';
-import { KbnServerError } from '../../../../../src/plugins/kibana_utils/server';
+import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server';
export const enhancedEsSearchStrategyProvider = (
config$: Observable,
@@ -41,7 +41,11 @@ export const enhancedEsSearchStrategyProvider = (
usage?: SearchUsage
): ISearchStrategy => {
async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) {
- await esClient.asCurrentUser.asyncSearch.delete({ id });
+ try {
+ await esClient.asCurrentUser.asyncSearch.delete({ id });
+ } catch (e) {
+ throw getKbnServerError(e);
+ }
}
function asyncSearch(
@@ -70,7 +74,10 @@ export const enhancedEsSearchStrategyProvider = (
return pollSearch(search, cancel, options).pipe(
tap((response) => (id = response.id)),
- tap(searchUsageObserver(logger, usage))
+ tap(searchUsageObserver(logger, usage)),
+ catchError((e) => {
+ throw getKbnServerError(e);
+ })
);
}
@@ -90,40 +97,72 @@ export const enhancedEsSearchStrategyProvider = (
...params,
};
- const promise = esClient.asCurrentUser.transport.request({
- method,
- path,
- body,
- querystring,
- });
+ try {
+ const promise = esClient.asCurrentUser.transport.request({
+ method,
+ path,
+ body,
+ querystring,
+ });
- const esResponse = await shimAbortSignal(promise, options?.abortSignal);
- const response = esResponse.body as SearchResponse;
- return {
- rawResponse: response,
- ...getTotalLoaded(response),
- };
+ const esResponse = await shimAbortSignal(promise, options?.abortSignal);
+ const response = esResponse.body as SearchResponse;
+ return {
+ rawResponse: response,
+ ...getTotalLoaded(response),
+ };
+ } catch (e) {
+ throw getKbnServerError(e);
+ }
}
return {
+ /**
+ * @param request
+ * @param options
+ * @param deps `SearchStrategyDependencies`
+ * @returns `Observable>`
+ * @throws `KbnServerError`
+ */
search: (request, options: IAsyncSearchOptions, deps) => {
logger.debug(`search ${JSON.stringify(request.params) || request.id}`);
+ if (request.indexType && request.indexType !== 'rollup') {
+ throw new KbnServerError('Unknown indexType', 400);
+ }
if (request.indexType === undefined) {
return asyncSearch(request, options, deps);
- } else if (request.indexType === 'rollup') {
- return from(rollupSearch(request, options, deps));
} else {
- throw new KbnServerError('Unknown indexType', 400);
+ return from(rollupSearch(request, options, deps));
}
},
+ /**
+ * @param id async search ID to cancel, as returned from _async_search API
+ * @param options
+ * @param deps `SearchStrategyDependencies`
+ * @returns `Promise`
+ * @throws `KbnServerError`
+ */
cancel: async (id, options, { esClient }) => {
logger.debug(`cancel ${id}`);
await cancelAsyncSearch(id, esClient);
},
+ /**
+ *
+ * @param id async search ID to extend, as returned from _async_search API
+ * @param keepAlive
+ * @param options
+ * @param deps `SearchStrategyDependencies`
+ * @returns `Promise`
+ * @throws `KbnServerError`
+ */
extend: async (id, keepAlive, options, { esClient }) => {
logger.debug(`extend ${id} by ${keepAlive}`);
- await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive });
+ try {
+ await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive });
+ } catch (e) {
+ throw getKbnServerError(e);
+ }
},
};
};
diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json
index c4b09276880d9..29bfd71cb32b4 100644
--- a/x-pack/plugins/data_enhanced/tsconfig.json
+++ b/x-pack/plugins/data_enhanced/tsconfig.json
@@ -14,7 +14,8 @@
"config.ts",
"../../../typings/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
- "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json"
+ "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json",
+ "common/search/test_data/*.json"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts
index 0c08b834a2778..2115976bcced1 100644
--- a/x-pack/test/api_integration/apis/search/search.ts
+++ b/x-pack/test/api_integration/apis/search/search.ts
@@ -6,6 +6,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
+import { verifyErrorResponse } from '../../../../../test/api_integration/apis/search/verify_error';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -90,6 +91,23 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp2.body.isRunning).to.be(false);
});
+ it('should fail without kbn-xref header', async () => {
+ const resp = await supertest
+ .post(`/internal/search/ese`)
+ .send({
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ },
+ })
+ .expect(400);
+
+ verifyErrorResponse(resp.body, 400, 'Request must contain a kbn-xsrf header.');
+ });
+
it('should return 400 when unknown index type is provided', async () => {
const resp = await supertest
.post(`/internal/search/ese`)
@@ -106,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(400);
- expect(resp.body.message).to.contain('Unknown indexType');
+ verifyErrorResponse(resp.body, 400, 'Unknown indexType');
});
it('should return 400 if invalid id is provided', async () => {
@@ -124,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(400);
- expect(resp.body.message).to.contain('illegal_argument_exception');
+ verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true);
});
it('should return 404 if unkown id is provided', async () => {
@@ -143,12 +161,11 @@ export default function ({ getService }: FtrProviderContext) {
},
})
.expect(404);
-
- expect(resp.body.message).to.contain('resource_not_found_exception');
+ verifyErrorResponse(resp.body, 404, 'resource_not_found_exception', true);
});
it('should return 400 with a bad body', async () => {
- await supertest
+ const resp = await supertest
.post(`/internal/search/ese`)
.set('kbn-xsrf', 'foo')
.send({
@@ -160,6 +177,8 @@ export default function ({ getService }: FtrProviderContext) {
},
})
.expect(400);
+
+ verifyErrorResponse(resp.body, 400, 'parsing_exception', true);
});
});
@@ -186,8 +205,7 @@ export default function ({ getService }: FtrProviderContext) {
},
})
.expect(400);
-
- expect(resp.body.message).to.contain('illegal_argument_exception');
+ verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true);
});
it('should return 400 if rollup search is without non-existent index', async () => {
@@ -207,7 +225,7 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(400);
- expect(resp.body.message).to.contain('illegal_argument_exception');
+ verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true);
});
it('should rollup search', async () => {
@@ -241,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.send()
.expect(400);
- expect(resp.body.message).to.contain('illegal_argument_exception');
+ verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true);
});
it('should delete a search', async () => {
From af337ce4edb6f09b69ab0513785c664be3e82f12 Mon Sep 17 00:00:00 2001
From: Clint Andrew Hall
Date: Sun, 31 Jan 2021 08:37:58 -0600
Subject: [PATCH 08/18] [Presentation Team] Migrate to Typescript Project
References (#86019)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/input_control_vis/tsconfig.json | 21 ++++++++
tsconfig.json | 1 +
tsconfig.refs.json | 11 ++--
.../server/demodata/get_demo_rows.ts | 2 +
.../renderers/error/index.tsx | 2 +-
.../filters/dropdown_filter/index.tsx | 2 +-
.../canvas_plugin_src/renderers/table.tsx | 2 +-
.../export/export/export_app.component.tsx | 2 +-
.../apps/home/home_app/home_app.component.tsx | 2 +-
.../workpad/workpad_app/workpad_telemetry.tsx | 2 +-
.../asset_manager/asset.component.tsx | 2 +-
.../asset_manager/asset_manager.component.tsx | 2 +-
.../confirm_modal/confirm_modal.tsx | 2 +-
.../page_preview/page_preview.component.tsx | 2 +-
.../components/toolbar/toolbar.component.tsx | 2 +-
.../workpad_config.component.tsx | 2 +-
.../refresh_control.component.tsx | 2 +-
.../canvas/public/functions/filters.ts | 2 +-
x-pack/plugins/canvas/public/functions/pie.ts | 2 +-
.../canvas/public/functions/plot/index.ts | 2 +-
.../canvas/public/functions/timelion.ts | 2 +-
x-pack/plugins/canvas/public/functions/to.ts | 2 +-
.../lib/template_from_react_component.tsx | 2 +-
.../canvas/server/sample_data/index.ts | 4 +-
.../shareable_runtime/context/actions.ts | 2 +-
.../canvas/shareable_runtime/test/index.ts | 3 ++
x-pack/plugins/canvas/tsconfig.json | 52 ++++++++++++++++++
x-pack/plugins/canvas/types/state.ts | 2 +-
.../server/routes/lib/get_document_payload.ts | 2 +-
x-pack/plugins/reporting/tsconfig.json | 31 +++++++++++
x-pack/tsconfig.json | 54 ++++++++++---------
x-pack/tsconfig.refs.json | 42 ++++++++-------
32 files changed, 190 insertions(+), 75 deletions(-)
create mode 100644 src/plugins/input_control_vis/tsconfig.json
create mode 100644 x-pack/plugins/canvas/tsconfig.json
create mode 100644 x-pack/plugins/reporting/tsconfig.json
diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json
new file mode 100644
index 0000000000000..bef7bc394a6cc
--- /dev/null
+++ b/src/plugins/input_control_vis/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "public/**/*",
+ "server/**/*",
+ ],
+ "references": [
+ { "path": "../kibana_react/tsconfig.json" },
+ { "path": "../data/tsconfig.json"},
+ { "path": "../expressions/tsconfig.json" },
+ { "path": "../visualizations/tsconfig.json" },
+ { "path": "../vis_default_editor/tsconfig.json" },
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 2647ac9a9d75e..d8fb2804242bc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,7 @@
"src/plugins/es_ui_shared/**/*",
"src/plugins/expressions/**/*",
"src/plugins/home/**/*",
+ "src/plugins/input_control_vis/**/*",
"src/plugins/inspector/**/*",
"src/plugins/kibana_legacy/**/*",
"src/plugins/kibana_react/**/*",
diff --git a/tsconfig.refs.json b/tsconfig.refs.json
index fa1b533a3dd38..9a65b385b7820 100644
--- a/tsconfig.refs.json
+++ b/tsconfig.refs.json
@@ -2,12 +2,12 @@
"include": [],
"references": [
{ "path": "./src/core/tsconfig.json" },
- { "path": "./src/plugins/telemetry_management_section/tsconfig.json" },
{ "path": "./src/plugins/advanced_settings/tsconfig.json" },
{ "path": "./src/plugins/apm_oss/tsconfig.json" },
{ "path": "./src/plugins/bfetch/tsconfig.json" },
{ "path": "./src/plugins/charts/tsconfig.json" },
{ "path": "./src/plugins/console/tsconfig.json" },
+ { "path": "./src/plugins/dashboard/tsconfig.json" },
{ "path": "./src/plugins/data/tsconfig.json" },
{ "path": "./src/plugins/dev_tools/tsconfig.json" },
{ "path": "./src/plugins/discover/tsconfig.json" },
@@ -15,8 +15,6 @@
{ "path": "./src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "./src/plugins/expressions/tsconfig.json" },
{ "path": "./src/plugins/home/tsconfig.json" },
- { "path": "./src/plugins/dashboard/tsconfig.json" },
- { "path": "./src/plugins/dev_tools/tsconfig.json" },
{ "path": "./src/plugins/inspector/tsconfig.json" },
{ "path": "./src/plugins/kibana_legacy/tsconfig.json" },
{ "path": "./src/plugins/kibana_react/tsconfig.json" },
@@ -26,16 +24,17 @@
{ "path": "./src/plugins/maps_legacy/tsconfig.json" },
{ "path": "./src/plugins/navigation/tsconfig.json" },
{ "path": "./src/plugins/newsfeed/tsconfig.json" },
+ { "path": "./src/plugins/presentation_util/tsconfig.json" },
{ "path": "./src/plugins/region_map/tsconfig.json" },
- { "path": "./src/plugins/saved_objects/tsconfig.json" },
{ "path": "./src/plugins/saved_objects_management/tsconfig.json" },
{ "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" },
- { "path": "./src/plugins/presentation_util/tsconfig.json" },
+ { "path": "./src/plugins/saved_objects/tsconfig.json" },
{ "path": "./src/plugins/security_oss/tsconfig.json" },
{ "path": "./src/plugins/share/tsconfig.json" },
{ "path": "./src/plugins/spaces_oss/tsconfig.json" },
- { "path": "./src/plugins/telemetry/tsconfig.json" },
{ "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "./src/plugins/telemetry_management_section/tsconfig.json" },
+ { "path": "./src/plugins/telemetry/tsconfig.json" },
{ "path": "./src/plugins/tile_map/tsconfig.json" },
{ "path": "./src/plugins/timelion/tsconfig.json" },
{ "path": "./src/plugins/ui_actions/tsconfig.json" },
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts
index 58a2354b5cf38..ff5a4506ab82a 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts
@@ -5,8 +5,10 @@
*/
import { cloneDeep } from 'lodash';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import ci from './ci.json';
import { DemoRows } from './demo_rows_types';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import shirts from './shirts.json';
import { getFunctionErrors } from '../../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx
index a9296bd9a1241..238b2edc3bd6d 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx
@@ -12,7 +12,7 @@ import { Popover } from '../../../public/components/popover';
import { RendererStrings } from '../../../i18n';
import { RendererFactory } from '../../../types';
-interface Config {
+export interface Config {
error: Error;
}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
index bfc36932a8a07..6c1dd086c8667 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
@@ -15,7 +15,7 @@ import { RendererStrings } from '../../../../i18n';
const { dropdownFilter: strings } = RendererStrings;
-interface Config {
+export interface Config {
/** The column to use within the exactly function */
column: string;
/**
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx
index ada159e07f6ae..4933b1b4ba51d 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx
@@ -12,7 +12,7 @@ import { RendererFactory, Style, Datatable } from '../../types';
const { dropdownFilter: strings } = RendererStrings;
-interface TableArguments {
+export interface TableArguments {
font?: Style;
paginate: boolean;
perPage: number;
diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx
index 03121e749d0dc..f26408b1200f1 100644
--- a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx
+++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx
@@ -13,7 +13,7 @@ import { WorkpadPage } from '../../../components/workpad_page';
import { Link } from '../../../components/link';
import { CanvasWorkpad } from '../../../../types';
-interface Props {
+export interface Props {
workpad: CanvasWorkpad;
selectedPageIndex: number;
initializeWorkpad: () => void;
diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
index 3c2e989cc8e51..7fbdc24c112a1 100644
--- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
+++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
@@ -11,7 +11,7 @@ import { WorkpadManager } from '../../../components/workpad_manager';
// @ts-expect-error untyped local
import { setDocTitle } from '../../../lib/doc_title';
-interface Props {
+export interface Props {
onLoad: () => void;
}
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
index 981334ff8d9f2..3697d5dad2dae 100644
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
@@ -46,7 +46,7 @@ interface ResolvedArgs {
[keys: string]: any;
}
-interface ElementsLoadedTelemetryProps extends PropsFromRedux {
+export interface ElementsLoadedTelemetryProps extends PropsFromRedux {
workpad: Workpad;
}
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
index ed000741bc542..d94802bf2a772 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
@@ -28,7 +28,7 @@ import { ComponentStrings } from '../../../i18n';
const { Asset: strings } = ComponentStrings;
-interface Props {
+export interface Props {
/** The asset to be rendered */
asset: AssetType;
/** The function to execute when the user clicks 'Create' */
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
index 98f3d8b48829d..6c1b546b49aa1 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
@@ -33,7 +33,7 @@ import { ComponentStrings } from '../../../i18n';
const { AssetManager: strings } = ComponentStrings;
-interface Props {
+export interface Props {
/** The assets to display within the modal */
assets: AssetType[];
/** Function to invoke when the modal is closed */
diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx
index 31a75acbba4ec..9d0a5e0a9f51d 100644
--- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx
+++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx
@@ -8,7 +8,7 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
-interface Props {
+export interface Props {
isOpen: boolean;
title?: string;
message: string;
diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx
index fd1dc869d60ec..da1fe8473e36d 100644
--- a/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx
+++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx
@@ -10,7 +10,7 @@ import { DomPreview } from '../dom_preview';
import { PageControls } from './page_controls';
import { CanvasPage } from '../../../types';
-interface Props {
+export interface Props {
isWriteable: boolean;
page: Pick;
height: number;
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
index 7151e72a44780..d33ba57050d4b 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
@@ -31,7 +31,7 @@ const { Toolbar: strings } = ComponentStrings;
type TrayType = 'pageManager' | 'expression';
-interface Props {
+export interface Props {
isWriteable: boolean;
selectedElement?: CanvasElement;
selectedPageNumber: number;
diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
index a7424882f1072..4068272bbaf11 100644
--- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
@@ -30,7 +30,7 @@ import { ComponentStrings } from '../../../i18n';
const { WorkpadConfig: strings } = ComponentStrings;
-interface Props {
+export interface Props {
size: {
height: number;
width: number;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
index d651e649128f9..023d87c7c3565 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
@@ -12,7 +12,7 @@ import { ToolTipShortcut } from '../../tool_tip_shortcut';
import { ComponentStrings } from '../../../../i18n';
const { WorkpadHeaderRefreshControlSettings: strings } = ComponentStrings;
-interface Props {
+export interface Props {
doRefresh: MouseEventHandler;
inFlight: boolean;
}
diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts
index fdb5d69d35515..70120ccad6f54 100644
--- a/x-pack/plugins/canvas/public/functions/filters.ts
+++ b/x-pack/plugins/canvas/public/functions/filters.ts
@@ -15,7 +15,7 @@ import { ExpressionValueFilter } from '../../types';
import { getFunctionHelp } from '../../i18n';
import { InitializeArguments } from '.';
-interface Arguments {
+export interface Arguments {
group: string[];
ungrouped: boolean;
}
diff --git a/x-pack/plugins/canvas/public/functions/pie.ts b/x-pack/plugins/canvas/public/functions/pie.ts
index ab3f1b932dc3c..e7cf153b9cd0f 100644
--- a/x-pack/plugins/canvas/public/functions/pie.ts
+++ b/x-pack/plugins/canvas/public/functions/pie.ts
@@ -61,7 +61,7 @@ export interface Pie {
options: PieOptions;
}
-interface Arguments {
+export interface Arguments {
palette: PaletteOutput;
seriesStyle: SeriesStyle[];
radius: number | 'auto';
diff --git a/x-pack/plugins/canvas/public/functions/plot/index.ts b/x-pack/plugins/canvas/public/functions/plot/index.ts
index a4661dc3401df..79aa11cfa2d80 100644
--- a/x-pack/plugins/canvas/public/functions/plot/index.ts
+++ b/x-pack/plugins/canvas/public/functions/plot/index.ts
@@ -17,7 +17,7 @@ import { getTickHash } from './get_tick_hash';
import { getFunctionHelp } from '../../../i18n';
import { AxisConfig, PointSeries, Render, SeriesStyle, Legend } from '../../../types';
-interface Arguments {
+export interface Arguments {
seriesStyle: SeriesStyle[];
defaultStyle: SeriesStyle;
palette: PaletteOutput;
diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts
index 947972fa310c9..3018540e5bf8e 100644
--- a/x-pack/plugins/canvas/public/functions/timelion.ts
+++ b/x-pack/plugins/canvas/public/functions/timelion.ts
@@ -15,7 +15,7 @@ import { Datatable, ExpressionValueFilter } from '../../types';
import { getFunctionHelp } from '../../i18n';
import { InitializeArguments } from './';
-interface Arguments {
+export interface Arguments {
query: string;
interval: string;
from: string;
diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts
index 36b2d3f9f04c6..c8ac4f714e5c4 100644
--- a/x-pack/plugins/canvas/public/functions/to.ts
+++ b/x-pack/plugins/canvas/public/functions/to.ts
@@ -10,7 +10,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { getFunctionHelp, getFunctionErrors } from '../../i18n';
import { InitializeArguments } from '.';
-interface Arguments {
+export interface Arguments {
type: string[];
}
diff --git a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx
index f4e715b1bbc49..95225cf13ff3b 100644
--- a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx
+++ b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx
@@ -11,7 +11,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { ErrorBoundary } from '../components/enhance/error_boundary';
import { ArgumentHandlers } from '../../types/arguments';
-interface Props {
+export interface Props {
renderError: Function;
}
diff --git a/x-pack/plugins/canvas/server/sample_data/index.ts b/x-pack/plugins/canvas/server/sample_data/index.ts
index 212d9f5132831..9c9ecb718fd5f 100644
--- a/x-pack/plugins/canvas/server/sample_data/index.ts
+++ b/x-pack/plugins/canvas/server/sample_data/index.ts
@@ -3,9 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import ecommerceSavedObjects from './ecommerce_saved_objects.json';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import flightsSavedObjects from './flights_saved_objects.json';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import webLogsSavedObjects from './web_logs_saved_objects.json';
import { loadSampleData } from './load_sample_data';
diff --git a/x-pack/plugins/canvas/shareable_runtime/context/actions.ts b/x-pack/plugins/canvas/shareable_runtime/context/actions.ts
index 8c88afbadfd9e..a36435688505d 100644
--- a/x-pack/plugins/canvas/shareable_runtime/context/actions.ts
+++ b/x-pack/plugins/canvas/shareable_runtime/context/actions.ts
@@ -17,7 +17,7 @@ export enum CanvasShareableActions {
SET_TOOLBAR_AUTOHIDE = 'SET_TOOLBAR_AUTOHIDE',
}
-interface FluxAction {
+export interface FluxAction {
type: T;
payload: P;
}
diff --git a/x-pack/plugins/canvas/shareable_runtime/test/index.ts b/x-pack/plugins/canvas/shareable_runtime/test/index.ts
index 288dd0dc3a5be..f0d2ebcc20128 100644
--- a/x-pack/plugins/canvas/shareable_runtime/test/index.ts
+++ b/x-pack/plugins/canvas/shareable_runtime/test/index.ts
@@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import hello from './workpads/hello.json';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import austin from './workpads/austin.json';
+// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config
import test from './workpads/test.json';
export * from './utils';
diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json
new file mode 100644
index 0000000000000..3e3986082e207
--- /dev/null
+++ b/x-pack/plugins/canvas/tsconfig.json
@@ -0,0 +1,52 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "../../../typings/**/*",
+ "__fixtures__/**/*",
+ "canvas_plugin_src/**/*",
+ "common/**/*",
+ "i18n/**/*",
+ "public/**/*",
+ "server/**/*",
+ "shareable_runtime/**/*",
+ "storybook/**/*",
+ "tasks/mocks/*",
+ "types/**/*",
+ "**/*.json",
+ ],
+ "exclude": [
+ // these files are too large and upset tsc, so we exclude them
+ "server/sample_data/*.json",
+ "canvas_plugin_src/functions/server/demodata/*.json",
+ "shareable_runtime/test/workpads/*.json",
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ { "path": "../../../src/plugins/bfetch/tsconfig.json"},
+ { "path": "../../../src/plugins/charts/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json"},
+ { "path": "../../../src/plugins/discover/tsconfig.json" },
+ { "path": "../../../src/plugins/embeddable/tsconfig.json" },
+ { "path": "../../../src/plugins/expressions/tsconfig.json" },
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ { "path": "../../../src/plugins/inspector/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
+ { "path": "../../../src/plugins/saved_objects/tsconfig.json" },
+ { "path": "../../../src/plugins/ui_actions/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../../../src/plugins/visualizations/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ { "path": "../lens/tsconfig.json" },
+ { "path": "../maps/tsconfig.json" },
+ { "path": "../reporting/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts
index 03bb931dc9b26..33f913563daac 100644
--- a/x-pack/plugins/canvas/types/state.ts
+++ b/x-pack/plugins/canvas/types/state.ts
@@ -52,7 +52,7 @@ type ExpressionType =
| Style
| Range;
-interface ExpressionRenderable {
+export interface ExpressionRenderable {
state: 'ready' | 'pending';
value: Render | null;
error: null;
diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts
index b154978d041f4..7706aa9d650c7 100644
--- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts
+++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts
@@ -13,7 +13,7 @@ import { ReportDocument } from '../../lib/store';
import { TaskRunResult } from '../../lib/tasks';
import { ExportTypeDefinition } from '../../types';
-interface ErrorFromPayload {
+export interface ErrorFromPayload {
message: string;
}
diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json
new file mode 100644
index 0000000000000..88e8d343f4700
--- /dev/null
+++ b/x-pack/plugins/reporting/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ "../../../typings/**/*"
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json"},
+ { "path": "../../../src/plugins/discover/tsconfig.json" },
+ { "path": "../../../src/plugins/embeddable/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../../../src/plugins/share/tsconfig.json" },
+ { "path": "../../../src/plugins/ui_actions/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../security/tsconfig.json" },
+ { "path": "../spaces/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json
index 4b161e3559849..1be6b5cf84cda 100644
--- a/x-pack/tsconfig.json
+++ b/x-pack/tsconfig.json
@@ -7,6 +7,7 @@
"plugins/apm/e2e/cypress/**/*",
"plugins/apm/ftr_e2e/**/*",
"plugins/apm/scripts/**/*",
+ "plugins/canvas/**/*",
"plugins/console_extensions/**/*",
"plugins/data_enhanced/**/*",
"plugins/discover_enhanced/**/*",
@@ -23,6 +24,7 @@
"plugins/maps/**/*",
"plugins/maps_file_upload/**/*",
"plugins/maps_legacy_licensing/**/*",
+ "plugins/reporting/**/*",
"plugins/searchprofiler/**/*",
"plugins/security_solution/cypress/**/*",
"plugins/task_manager/**/*",
@@ -49,15 +51,13 @@
},
"references": [
{ "path": "../src/core/tsconfig.json" },
- { "path": "../src/plugins/telemetry_management_section/tsconfig.json" },
- { "path": "../src/plugins/management/tsconfig.json" },
{ "path": "../src/plugins/bfetch/tsconfig.json" },
{ "path": "../src/plugins/charts/tsconfig.json" },
{ "path": "../src/plugins/console/tsconfig.json" },
{ "path": "../src/plugins/dashboard/tsconfig.json" },
- { "path": "../src/plugins/discover/tsconfig.json" },
{ "path": "../src/plugins/data/tsconfig.json" },
{ "path": "../src/plugins/dev_tools/tsconfig.json" },
+ { "path": "../src/plugins/discover/tsconfig.json" },
{ "path": "../src/plugins/embeddable/tsconfig.json" },
{ "path": "../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../src/plugins/expressions/tsconfig.json" },
@@ -67,53 +67,55 @@
{ "path": "../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../src/plugins/kibana_usage_collection/tsconfig.json" },
{ "path": "../src/plugins/kibana_utils/tsconfig.json" },
+ { "path": "../src/plugins/management/tsconfig.json" },
{ "path": "../src/plugins/navigation/tsconfig.json" },
{ "path": "../src/plugins/newsfeed/tsconfig.json" },
- { "path": "../src/plugins/saved_objects/tsconfig.json" },
+ { "path": "../src/plugins/presentation_util/tsconfig.json" },
{ "path": "../src/plugins/saved_objects_management/tsconfig.json" },
{ "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" },
- { "path": "../src/plugins/presentation_util/tsconfig.json" },
+ { "path": "../src/plugins/saved_objects/tsconfig.json" },
{ "path": "../src/plugins/security_oss/tsconfig.json" },
{ "path": "../src/plugins/share/tsconfig.json" },
- { "path": "../src/plugins/telemetry/tsconfig.json" },
{ "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" },
- { "path": "../src/plugins/url_forwarding/tsconfig.json" },
+ { "path": "../src/plugins/telemetry_management_section/tsconfig.json" },
+ { "path": "../src/plugins/telemetry/tsconfig.json" },
{ "path": "../src/plugins/ui_actions/tsconfig.json" },
{ "path": "../src/plugins/url_forwarding/tsconfig.json" },
{ "path": "../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "./plugins/actions/tsconfig.json"},
+ { "path": "./plugins/alerts/tsconfig.json"},
+ { "path": "./plugins/beats_management/tsconfig.json" },
+ { "path": "./plugins/canvas/tsconfig.json" },
+ { "path": "./plugins/cloud/tsconfig.json" },
{ "path": "./plugins/console_extensions/tsconfig.json" },
{ "path": "./plugins/data_enhanced/tsconfig.json" },
{ "path": "./plugins/discover_enhanced/tsconfig.json" },
- { "path": "./plugins/global_search/tsconfig.json" },
- { "path": "./plugins/global_search_providers/tsconfig.json" },
+ { "path": "./plugins/embeddable_enhanced/tsconfig.json" },
+ { "path": "./plugins/encrypted_saved_objects/tsconfig.json" },
{ "path": "./plugins/enterprise_search/tsconfig.json" },
+ { "path": "./plugins/event_log/tsconfig.json" },
{ "path": "./plugins/features/tsconfig.json" },
+ { "path": "./plugins/global_search_bar/tsconfig.json" },
+ { "path": "./plugins/global_search_providers/tsconfig.json" },
+ { "path": "./plugins/global_search/tsconfig.json" },
{ "path": "./plugins/graph/tsconfig.json" },
- { "path": "./plugins/embeddable_enhanced/tsconfig.json" },
- { "path": "./plugins/event_log/tsconfig.json" },
- { "path": "./plugins/licensing/tsconfig.json" },
{ "path": "./plugins/lens/tsconfig.json" },
- { "path": "./plugins/maps/tsconfig.json" },
+ { "path": "./plugins/license_management/tsconfig.json" },
+ { "path": "./plugins/licensing/tsconfig.json" },
{ "path": "./plugins/maps_file_upload/tsconfig.json" },
{ "path": "./plugins/maps_legacy_licensing/tsconfig.json" },
+ { "path": "./plugins/maps/tsconfig.json" },
+ { "path": "./plugins/painless_lab/tsconfig.json" },
+ { "path": "./plugins/saved_objects_tagging/tsconfig.json" },
{ "path": "./plugins/searchprofiler/tsconfig.json" },
+ { "path": "./plugins/security/tsconfig.json" },
+ { "path": "./plugins/spaces/tsconfig.json" },
+ { "path": "./plugins/stack_alerts/tsconfig.json"},
{ "path": "./plugins/task_manager/tsconfig.json" },
{ "path": "./plugins/telemetry_collection_xpack/tsconfig.json" },
- { "path": "./plugins/ui_actions_enhanced/tsconfig.json" },
{ "path": "./plugins/translations/tsconfig.json" },
- { "path": "./plugins/spaces/tsconfig.json" },
- { "path": "./plugins/security/tsconfig.json" },
- { "path": "./plugins/encrypted_saved_objects/tsconfig.json" },
- { "path": "./plugins/beats_management/tsconfig.json" },
- { "path": "./plugins/cloud/tsconfig.json" },
- { "path": "./plugins/saved_objects_tagging/tsconfig.json" },
- { "path": "./plugins/global_search_bar/tsconfig.json" },
- { "path": "./plugins/actions/tsconfig.json"},
- { "path": "./plugins/alerts/tsconfig.json"},
{ "path": "./plugins/triggers_actions_ui/tsconfig.json"},
- { "path": "./plugins/stack_alerts/tsconfig.json"},
- { "path": "./plugins/license_management/tsconfig.json" },
- { "path": "./plugins/painless_lab/tsconfig.json" },
+ { "path": "./plugins/ui_actions_enhanced/tsconfig.json" },
{ "path": "./plugins/watcher/tsconfig.json" },
]
}
diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json
index f5b35c9429a1c..ed209cd241586 100644
--- a/x-pack/tsconfig.refs.json
+++ b/x-pack/tsconfig.refs.json
@@ -3,38 +3,40 @@
"references": [
{ "path": "./plugins/actions/tsconfig.json"},
{ "path": "./plugins/alerts/tsconfig.json"},
- { "path": "./plugins/dashboard_enhanced/tsconfig.json" },
- { "path": "./plugins/licensing/tsconfig.json" },
- { "path": "./plugins/lens/tsconfig.json" },
+ { "path": "./plugins/beats_management/tsconfig.json" },
+ { "path": "./plugins/canvas/tsconfig.json" },
+ { "path": "./plugins/cloud/tsconfig.json" },
{ "path": "./plugins/console_extensions/tsconfig.json" },
- { "path": "./plugins/discover_enhanced/tsconfig.json" },
+ { "path": "./plugins/dashboard_enhanced/tsconfig.json" },
{ "path": "./plugins/data_enhanced/tsconfig.json" },
- { "path": "./plugins/global_search/tsconfig.json" },
- { "path": "./plugins/global_search_providers/tsconfig.json" },
+ { "path": "./plugins/discover_enhanced/tsconfig.json" },
+ { "path": "./plugins/embeddable_enhanced/tsconfig.json" },
+ { "path": "./plugins/encrypted_saved_objects/tsconfig.json" },
+ { "path": "./plugins/enterprise_search/tsconfig.json" },
{ "path": "./plugins/event_log/tsconfig.json"},
{ "path": "./plugins/features/tsconfig.json" },
+ { "path": "./plugins/global_search_bar/tsconfig.json" },
+ { "path": "./plugins/global_search_providers/tsconfig.json" },
+ { "path": "./plugins/global_search/tsconfig.json" },
{ "path": "./plugins/graph/tsconfig.json" },
- { "path": "./plugins/embeddable_enhanced/tsconfig.json" },
- { "path": "./plugins/enterprise_search/tsconfig.json" },
- { "path": "./plugins/maps/tsconfig.json" },
+ { "path": "./plugins/lens/tsconfig.json" },
+ { "path": "./plugins/license_management/tsconfig.json" },
+ { "path": "./plugins/licensing/tsconfig.json" },
{ "path": "./plugins/maps_file_upload/tsconfig.json" },
{ "path": "./plugins/maps_legacy_licensing/tsconfig.json" },
+ { "path": "./plugins/maps/tsconfig.json" },
+ { "path": "./plugins/painless_lab/tsconfig.json" },
+ { "path": "./plugins/reporting/tsconfig.json" },
+ { "path": "./plugins/saved_objects_tagging/tsconfig.json" },
{ "path": "./plugins/searchprofiler/tsconfig.json" },
+ { "path": "./plugins/security/tsconfig.json" },
+ { "path": "./plugins/spaces/tsconfig.json" },
+ { "path": "./plugins/stack_alerts/tsconfig.json"},
{ "path": "./plugins/task_manager/tsconfig.json" },
{ "path": "./plugins/telemetry_collection_xpack/tsconfig.json" },
- { "path": "./plugins/ui_actions_enhanced/tsconfig.json" },
{ "path": "./plugins/translations/tsconfig.json" },
{ "path": "./plugins/triggers_actions_ui/tsconfig.json"},
- { "path": "./plugins/spaces/tsconfig.json" },
- { "path": "./plugins/security/tsconfig.json" },
- { "path": "./plugins/stack_alerts/tsconfig.json"},
- { "path": "./plugins/encrypted_saved_objects/tsconfig.json" },
- { "path": "./plugins/beats_management/tsconfig.json" },
- { "path": "./plugins/cloud/tsconfig.json" },
- { "path": "./plugins/saved_objects_tagging/tsconfig.json" },
- { "path": "./plugins/global_search_bar/tsconfig.json" },
- { "path": "./plugins/license_management/tsconfig.json" },
- { "path": "./plugins/painless_lab/tsconfig.json" },
+ { "path": "./plugins/ui_actions_enhanced/tsconfig.json" },
{ "path": "./plugins/watcher/tsconfig.json" }
]
}
From 4f43096c64c4b27205ecd8fd3aecfd1426da6892 Mon Sep 17 00:00:00 2001
From: Nicolas Ruflin
Date: Mon, 1 Feb 2021 10:28:49 +0100
Subject: [PATCH 09/18] [Fleet] Remove comments around experimental registry
(#89830)
The experimental registry was used for the 7.8 release but since then was not touched anymore. Because of this it should not show up in the code anymore even if it is commented out.
---
.../plugins/fleet/server/services/epm/registry/registry_url.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
index efc25cc2efb5d..4f17a2b88670a 100644
--- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
+++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
@@ -11,12 +11,10 @@ import { appContextService, licenseService } from '../../';
const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co';
// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co';
-// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/';
const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co';
// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev';
// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev';
-// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/';
// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev';
const getDefaultRegistryUrl = (): string => {
From c2f53a96ebb6e4a5a9a8e4dcbbcde33aa4d1f20d Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Mon, 1 Feb 2021 10:40:38 +0100
Subject: [PATCH 10/18] [Search Sessions][Dashboard] Clear search session when
navigating from dashboard route (#89749)
---
src/plugins/dashboard/public/application/dashboard_app.tsx | 7 +++++++
.../apps/dashboard/async_search/send_to_background.ts | 4 ++++
2 files changed, 11 insertions(+)
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 7ea181715717b..6955365ebca3f 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -265,6 +265,13 @@ export function DashboardApp({
};
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
+ // clear search session when leaving dashboard route
+ useEffect(() => {
+ return () => {
+ data.search.session.clear();
+ };
+ }, [data.search.session]);
+
return (
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
index 7e878e763bfc1..3e417551c3cb9 100644
--- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
+++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
@@ -96,6 +96,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// should leave session state untouched
await PageObjects.dashboard.switchToEditMode();
await searchSessions.expectState('restored');
+
+ // navigating to a listing page clears the session
+ await PageObjects.dashboard.gotoDashboardLandingPage();
+ await searchSessions.missingOrFail();
});
});
}
From f0717a0a79d8cb1c772a9039aa7796691aa78ea0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Casper=20H=C3=BCbertz?=
Date: Mon, 1 Feb 2021 10:54:08 +0100
Subject: [PATCH 11/18] [Observability] `ActionMenu` style fixes (#89547)
* [Observability] Reduced space between title and subtitle
* [Observability] Reduce margin between sections
* [Observability] Reduce list item font size
* [Observability] Remove spacer
* [APM] Changes button style and label
* [Logs] Changes the actions button label and style
* [Logs] Fixes the overlap of actions button and close
* Updated test and snapshot
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../CustomLinkMenuSection/index.tsx | 1 -
.../TransactionActionMenu.test.tsx | 12 ++++++------
.../TransactionActionMenu/TransactionActionMenu.tsx | 8 ++++----
.../TransactionActionMenu.test.tsx.snap | 8 ++++----
.../log_entry_flyout/log_entry_actions_menu.tsx | 8 ++++----
.../logging/log_entry_flyout/log_entry_flyout.tsx | 2 +-
.../public/components/shared/action_menu/index.tsx | 6 +++---
7 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
index ae22718af8b57..43f566a93a89d 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
@@ -107,7 +107,6 @@ export function CustomLinkMenuSection({
-
{i18n.translate(
'xpack.apm.transactionActionMenu.customLink.subtitle',
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
index 48c863b460482..3141dc7a5f3c6 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
@@ -52,7 +52,7 @@ const renderTransaction = async (transaction: Record) => {
}
);
- fireEvent.click(rendered.getByText('Actions'));
+ fireEvent.click(rendered.getByText('Investigate'));
return rendered;
};
@@ -289,7 +289,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsNotInDocument(component, ['Custom Links']);
});
@@ -313,7 +313,7 @@ describe('TransactionActionMenu component', () => {
{ wrapper: Wrapper }
);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsNotInDocument(component, ['Custom Links']);
});
@@ -330,7 +330,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
});
@@ -347,7 +347,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
});
@@ -364,7 +364,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
act(() => {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
index 312513db80886..22fa25f93b212 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonEmpty } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
@@ -30,11 +30,11 @@ interface Props {
function ActionMenuButton({ onClick }: { onClick: () => void }) {
return (
-
+
{i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', {
- defaultMessage: 'Actions',
+ defaultMessage: 'Investigate',
})}
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
index fa6db645d28a8..ea33fb3c3df08 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
@@ -10,20 +10,20 @@ exports[`TransactionActionMenu component matches the snapshot 1`] = `
class="euiPopover__anchor"
>
diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
index aa3b4532e878e..9fef939733432 100644
--- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
+import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import { useVisibilityState } from '../../../utils/use_visibility_state';
@@ -67,7 +67,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{
-
+
}
closePopover={hide}
id="logEntryActionsMenu"
diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
index 5684d4068f3be..7d8ca95f9b93b 100644
--- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
@@ -88,7 +88,7 @@ export const LogEntryFlyout = ({
>
) : null}
-
+
{logEntry ? : null}
diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
index 4819a0760d88a..af61f618a89b2 100644
--- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
@@ -26,7 +26,7 @@ export function SectionTitle({ children }: { children?: ReactNode }) {
{children}
-
+
>
);
}
@@ -55,7 +55,7 @@ export function SectionSpacer() {
}
export const Section = styled.div`
- margin-bottom: 24px;
+ margin-bottom: 16px;
&:last-of-type {
margin-bottom: 0;
}
@@ -63,7 +63,7 @@ export const Section = styled.div`
export type SectionLinkProps = EuiListGroupItemProps;
export function SectionLink(props: SectionLinkProps) {
- return ;
+ return ;
}
export function ActionMenuDivider() {
From 84d49f11238c76c806b360a28f1d579dde38ab16 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Mon, 1 Feb 2021 11:03:44 +0100
Subject: [PATCH 12/18] [SOM] display invalid references in the relationship
flyout (#88814)
* return invalid relations and display them in SOM
* add FTR test
---
.../saved_objects_management/common/index.ts | 9 +-
.../saved_objects_management/common/types.ts | 16 +-
.../public/lib/get_relationships.test.ts | 9 +-
.../public/lib/get_relationships.ts | 6 +-
.../__snapshots__/relationships.test.tsx.snap | 1097 ++++++++++-------
.../components/relationships.test.tsx | 265 ++--
.../components/relationships.tsx | 179 ++-
.../saved_objects_management/public/types.ts | 9 +-
.../server/lib/find_relationships.test.ts | 227 +++-
.../server/lib/find_relationships.ts | 73 +-
.../server/routes/relationships.ts | 4 +-
.../saved_objects_management/server/types.ts | 9 +-
.../saved_objects_management/relationships.ts | 106 +-
.../saved_objects/relationships/data.json | 190 +++
.../saved_objects/relationships/data.json.gz | Bin 1385 -> 0 bytes
.../saved_objects/relationships/mappings.json | 16 +-
.../apps/saved_objects_management/index.ts | 1 +
.../show_relationships.ts | 52 +
.../show_relationships/data.json | 36 +
.../show_relationships/mappings.json | 473 +++++++
.../management/saved_objects_page.ts | 16 +
21 files changed, 2058 insertions(+), 735 deletions(-)
create mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
delete mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz
create mode 100644 test/functional/apps/saved_objects_management/show_relationships.ts
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts
index a8395e602979c..8850899e38958 100644
--- a/src/plugins/saved_objects_management/common/index.ts
+++ b/src/plugins/saved_objects_management/common/index.ts
@@ -6,4 +6,11 @@
* Public License, v 1.
*/
-export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';
+export {
+ SavedObjectWithMetadata,
+ SavedObjectMetadata,
+ SavedObjectRelation,
+ SavedObjectRelationKind,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from './types';
diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts
index 8618cf4332acf..e100dfc6b23e6 100644
--- a/src/plugins/saved_objects_management/common/types.ts
+++ b/src/plugins/saved_objects_management/common/types.ts
@@ -28,12 +28,26 @@ export type SavedObjectWithMetadata = SavedObject & {
meta: SavedObjectMetadata;
};
+export type SavedObjectRelationKind = 'child' | 'parent';
+
/**
* Represents a relation between two {@link SavedObject | saved object}
*/
export interface SavedObjectRelation {
id: string;
type: string;
- relationship: 'child' | 'parent';
+ relationship: SavedObjectRelationKind;
meta: SavedObjectMetadata;
}
+
+export interface SavedObjectInvalidRelation {
+ id: string;
+ type: string;
+ relationship: SavedObjectRelationKind;
+ error: string;
+}
+
+export interface SavedObjectGetRelationshipsResponse {
+ relations: SavedObjectRelation[];
+ invalidRelations: SavedObjectInvalidRelation[];
+}
diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
index b609fac67dac1..4454907f530fe 100644
--- a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
+++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
@@ -6,6 +6,7 @@
* Public License, v 1.
*/
+import { SavedObjectGetRelationshipsResponse } from '../types';
import { httpServiceMock } from '../../../../core/public/mocks';
import { getRelationships } from './get_relationships';
@@ -22,13 +23,17 @@ describe('getRelationships', () => {
});
it('should handle successful responses', async () => {
- httpMock.get.mockResolvedValue([1, 2]);
+ const serverResponse: SavedObjectGetRelationshipsResponse = {
+ relations: [],
+ invalidRelations: [],
+ };
+ httpMock.get.mockResolvedValue(serverResponse);
const response = await getRelationships(httpMock, 'dashboard', '1', [
'search',
'index-pattern',
]);
- expect(response).toEqual([1, 2]);
+ expect(response).toEqual(serverResponse);
});
it('should handle errors', async () => {
diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts
index 0eb97e1052fa4..69aeb6fbf580b 100644
--- a/src/plugins/saved_objects_management/public/lib/get_relationships.ts
+++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts
@@ -8,19 +8,19 @@
import { HttpStart } from 'src/core/public';
import { get } from 'lodash';
-import { SavedObjectRelation } from '../types';
+import { SavedObjectGetRelationshipsResponse } from '../types';
export async function getRelationships(
http: HttpStart,
type: string,
id: string,
savedObjectTypes: string[]
-): Promise {
+): Promise {
const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
type
)}/${encodeURIComponent(id)}`;
try {
- return await http.get(url, {
+ return await http.get(url, {
query: {
savedObjectTypes,
},
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
index 15e5cb89b622c..c39263f304249 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
@@ -28,133 +28,131 @@ exports[`Relationships should render dashboards normally 1`] = `
-
-
-
- Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -231,138 +229,315 @@ exports[`Relationships should render index patterns normally 1`] = `
-
-
-
- Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
+
+
+
+
+
+
+`;
+
+exports[`Relationships should render invalid relations 1`] = `
+
+
+
+
+
+
+
+
+ MyIndexPattern*
+
+
+
+
+
+
+
+
+
+
+ Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -395,138 +570,136 @@ exports[`Relationships should render searches normally 1`] = `
-
-
-
- Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -559,133 +732,131 @@ exports[`Relationships should render visualizations normally 1`] = `
-
-
-
- Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
index 72a4b0f2788fa..e590520193bba 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
@@ -25,36 +25,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'search',
- id: '1',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedSearches/1',
- icon: 'search',
- inAppUrl: {
- path: '/app/discover#//1',
- uiCapabilitiesPath: 'discover.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'search',
+ id: '1',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedSearches/1',
+ icon: 'search',
+ inAppUrl: {
+ path: '/app/discover#//1',
+ uiCapabilitiesPath: 'discover.show',
+ },
+ title: 'My Search Title',
},
- title: 'My Search Title',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title',
},
- title: 'My Visualization Title',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'index-pattern',
@@ -92,36 +95,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'index-pattern',
- id: '1',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/indexPatterns/patterns/1',
- icon: 'indexPatternApp',
- inAppUrl: {
- path: '/app/management/kibana/indexPatterns/patterns/1',
- uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'index-pattern',
+ id: '1',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/indexPatterns/patterns/1',
+ icon: 'indexPatternApp',
+ inAppUrl: {
+ path: '/app/management/kibana/indexPatterns/patterns/1',
+ uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ },
+ title: 'My Index Pattern',
},
- title: 'My Index Pattern',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title',
},
- title: 'My Visualization Title',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'search',
@@ -159,36 +165,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'dashboard',
- id: '1',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedDashboards/1',
- icon: 'dashboardApp',
- inAppUrl: {
- path: '/app/kibana#/dashboard/1',
- uiCapabilitiesPath: 'dashboard.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'dashboard',
+ id: '1',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedDashboards/1',
+ icon: 'dashboardApp',
+ inAppUrl: {
+ path: '/app/kibana#/dashboard/1',
+ uiCapabilitiesPath: 'dashboard.show',
+ },
+ title: 'My Dashboard 1',
},
- title: 'My Dashboard 1',
},
- },
- {
- type: 'dashboard',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedDashboards/2',
- icon: 'dashboardApp',
- inAppUrl: {
- path: '/app/kibana#/dashboard/2',
- uiCapabilitiesPath: 'dashboard.show',
+ {
+ type: 'dashboard',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedDashboards/2',
+ icon: 'dashboardApp',
+ inAppUrl: {
+ path: '/app/kibana#/dashboard/2',
+ uiCapabilitiesPath: 'dashboard.show',
+ },
+ title: 'My Dashboard 2',
},
- title: 'My Dashboard 2',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'visualization',
@@ -226,36 +235,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'visualization',
- id: '1',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/1',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/1',
- uiCapabilitiesPath: 'visualize.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'visualization',
+ id: '1',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/1',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/1',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title 1',
},
- title: 'My Visualization Title 1',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title 2',
},
- title: 'My Visualization Title 2',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'dashboard',
@@ -324,4 +336,49 @@ describe('Relationships', () => {
expect(props.getRelationships).toHaveBeenCalled();
expect(component).toMatchSnapshot();
});
+
+ it('should render invalid relations', async () => {
+ const props: RelationshipsProps = {
+ goInspectObject: () => {},
+ canGoInApp: () => true,
+ basePath: httpServiceMock.createSetupContract().basePath,
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [],
+ invalidRelations: [
+ {
+ id: '1',
+ type: 'dashboard',
+ relationship: 'child',
+ error: 'Saved object [dashboard/1] not found',
+ },
+ ],
+ })),
+ savedObject: {
+ id: '1',
+ type: 'index-pattern',
+ attributes: {},
+ references: [],
+ meta: {
+ title: 'MyIndexPattern*',
+ icon: 'indexPatternApp',
+ editUrl: '#/management/kibana/indexPatterns/patterns/1',
+ inAppUrl: {
+ path: '/management/kibana/indexPatterns/patterns/1',
+ uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ },
+ },
+ },
+ close: jest.fn(),
+ };
+
+ const component = shallowWithI18nProvider();
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(props.getRelationships).toHaveBeenCalled();
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
index 2d62699b6f1f2..aee61f7bc9c7a 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
@@ -26,11 +26,17 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { IBasePath } from 'src/core/public';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
-import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types';
+import {
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../../../types';
export interface RelationshipsProps {
basePath: IBasePath;
- getRelationships: (type: string, id: string) => Promise;
+ getRelationships: (type: string, id: string) => Promise;
savedObject: SavedObjectWithMetadata;
close: () => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
@@ -38,17 +44,47 @@ export interface RelationshipsProps {
}
export interface RelationshipsState {
- relationships: SavedObjectRelation[];
+ relations: SavedObjectRelation[];
+ invalidRelations: SavedObjectInvalidRelation[];
isLoading: boolean;
error?: string;
}
+const relationshipColumn = {
+ field: 'relationship',
+ name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnRelationshipName', {
+ defaultMessage: 'Direct relationship',
+ }),
+ dataType: 'string',
+ sortable: false,
+ width: '125px',
+ 'data-test-subj': 'directRelationship',
+ render: (relationship: SavedObjectRelationKind) => {
+ return (
+
+ {relationship === 'parent' ? (
+
+ ) : (
+
+ )}
+
+ );
+ },
+};
+
export class Relationships extends Component {
constructor(props: RelationshipsProps) {
super(props);
this.state = {
- relationships: [],
+ relations: [],
+ invalidRelations: [],
isLoading: false,
error: undefined,
};
@@ -70,8 +106,11 @@ export class Relationships extends Component
+
+
+ ({
+ 'data-test-subj': `invalidRelationshipsTableRow`,
+ })}
+ />
+
+ >
+ );
+ }
+
+ renderRelationshipsTable() {
+ const { goInspectObject, basePath, savedObject } = this.props;
+ const { relations, isLoading, error } = this.state;
if (error) {
return this.renderError();
@@ -137,39 +250,7 @@ export class Relationships extends Component {
- if (relationship === 'parent') {
- return (
-
-
-
- );
- }
- if (relationship === 'child') {
- return (
-
-
-
- );
- }
- },
- },
+ relationshipColumn,
{
field: 'meta.title',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', {
@@ -224,7 +305,7 @@ export class Relationships extends Component [
+ relations.map((relationship) => [
relationship.type,
{
value: relationship.type,
@@ -277,7 +358,7 @@ export class Relationships extends Component
+ <>
{i18n.translate(
@@ -296,7 +377,7 @@ export class Relationships extends Component
-
+ >
);
}
@@ -328,8 +409,10 @@ export class Relationships extends Component
-
- {this.renderRelationships()}
+
+ {this.renderInvalidRelationship()}
+ {this.renderRelationshipsTable()}
+
);
}
diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts
index 37f239227475d..cdfa3c43e5af2 100644
--- a/src/plugins/saved_objects_management/public/types.ts
+++ b/src/plugins/saved_objects_management/public/types.ts
@@ -6,4 +6,11 @@
* Public License, v 1.
*/
-export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
+export {
+ SavedObjectMetadata,
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../common';
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
index 631faf0c23c98..416be7d7e7426 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
@@ -6,10 +6,35 @@
* Public License, v 1.
*/
+import type { SavedObject, SavedObjectError } from 'src/core/types';
+import type { SavedObjectsFindResponse } from 'src/core/server';
import { findRelationships } from './find_relationships';
import { managementMock } from '../services/management.mock';
import { savedObjectsClientMock } from '../../../../core/server/mocks';
+const createObj = (parts: Partial>): SavedObject => ({
+ id: 'id',
+ type: 'type',
+ attributes: {},
+ references: [],
+ ...parts,
+});
+
+const createFindResponse = (objs: SavedObject[]): SavedObjectsFindResponse => ({
+ saved_objects: objs.map((obj) => ({ ...obj, score: 1 })),
+ total: objs.length,
+ per_page: 20,
+ page: 1,
+});
+
+const createError = (parts: Partial): SavedObjectError => ({
+ error: 'error',
+ message: 'message',
+ metadata: {},
+ statusCode: 404,
+ ...parts,
+});
+
describe('findRelationships', () => {
let savedObjectsClient: ReturnType;
let managementService: ReturnType;
@@ -19,7 +44,7 @@ describe('findRelationships', () => {
managementService = managementMock.create();
});
- it('returns the child and parent references of the object', async () => {
+ it('calls the savedObjectClient APIs with the correct parameters', async () => {
const type = 'dashboard';
const id = 'some-id';
const references = [
@@ -36,46 +61,35 @@ describe('findRelationships', () => {
];
const referenceTypes = ['some-type', 'another-type'];
- savedObjectsClient.get.mockResolvedValue({
- id,
- type,
- attributes: {},
- references,
- });
-
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
- {
+ createObj({
type: 'some-type',
id: 'ref-1',
- attributes: {},
- references: [],
- },
- {
+ }),
+ createObj({
type: 'another-type',
id: 'ref-2',
- attributes: {},
- references: [],
- },
+ }),
],
});
-
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [
- {
+ savedObjectsClient.find.mockResolvedValue(
+ createFindResponse([
+ createObj({
type: 'parent-type',
id: 'parent-id',
- attributes: {},
- score: 1,
- references: [],
- },
- ],
- total: 1,
- per_page: 20,
- page: 1,
- });
+ }),
+ ])
+ );
- const relationships = await findRelationships({
+ await findRelationships({
type,
id,
size: 20,
@@ -101,8 +115,63 @@ describe('findRelationships', () => {
perPage: 20,
type: referenceTypes,
});
+ });
+
+ it('returns the child and parent references of the object', async () => {
+ const type = 'dashboard';
+ const id = 'some-id';
+ const references = [
+ {
+ type: 'some-type',
+ id: 'ref-1',
+ name: 'ref 1',
+ },
+ {
+ type: 'another-type',
+ id: 'ref-2',
+ name: 'ref 2',
+ },
+ ];
+ const referenceTypes = ['some-type', 'another-type'];
+
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
+ savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [
+ createObj({
+ type: 'some-type',
+ id: 'ref-1',
+ }),
+ createObj({
+ type: 'another-type',
+ id: 'ref-2',
+ }),
+ ],
+ });
+ savedObjectsClient.find.mockResolvedValue(
+ createFindResponse([
+ createObj({
+ type: 'parent-type',
+ id: 'parent-id',
+ }),
+ ])
+ );
+
+ const { relations, invalidRelations } = await findRelationships({
+ type,
+ id,
+ size: 20,
+ client: savedObjectsClient,
+ referenceTypes,
+ savedObjectsManagement: managementService,
+ });
- expect(relationships).toEqual([
+ expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',
@@ -122,6 +191,70 @@ describe('findRelationships', () => {
meta: expect.any(Object),
},
]);
+ expect(invalidRelations).toHaveLength(0);
+ });
+
+ it('returns the invalid relations', async () => {
+ const type = 'dashboard';
+ const id = 'some-id';
+ const references = [
+ {
+ type: 'some-type',
+ id: 'ref-1',
+ name: 'ref 1',
+ },
+ {
+ type: 'another-type',
+ id: 'ref-2',
+ name: 'ref 2',
+ },
+ ];
+ const referenceTypes = ['some-type', 'another-type'];
+
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
+ const ref1Error = createError({ message: 'Not found' });
+ savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [
+ createObj({
+ type: 'some-type',
+ id: 'ref-1',
+ error: ref1Error,
+ }),
+ createObj({
+ type: 'another-type',
+ id: 'ref-2',
+ }),
+ ],
+ });
+ savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
+
+ const { relations, invalidRelations } = await findRelationships({
+ type,
+ id,
+ size: 20,
+ client: savedObjectsClient,
+ referenceTypes,
+ savedObjectsManagement: managementService,
+ });
+
+ expect(relations).toEqual([
+ {
+ id: 'ref-2',
+ relationship: 'child',
+ type: 'another-type',
+ meta: expect.any(Object),
+ },
+ ]);
+
+ expect(invalidRelations).toEqual([
+ { type: 'some-type', id: 'ref-1', relationship: 'child', error: ref1Error.message },
+ ]);
});
it('uses the management service to consolidate the relationship objects', async () => {
@@ -144,32 +277,24 @@ describe('findRelationships', () => {
uiCapabilitiesPath: 'uiCapabilitiesPath',
});
- savedObjectsClient.get.mockResolvedValue({
- id,
- type,
- attributes: {},
- references,
- });
-
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
- {
+ createObj({
type: 'some-type',
id: 'ref-1',
- attributes: {},
- references: [],
- },
+ }),
],
});
+ savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [],
- total: 0,
- per_page: 20,
- page: 1,
- });
-
- const relationships = await findRelationships({
+ const { relations } = await findRelationships({
type,
id,
size: 20,
@@ -183,7 +308,7 @@ describe('findRelationships', () => {
expect(managementService.getEditUrl).toHaveBeenCalledTimes(1);
expect(managementService.getInAppUrl).toHaveBeenCalledTimes(1);
- expect(relationships).toEqual([
+ expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts
index 0ceef484196a3..bc6568e73c4e2 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts
@@ -9,7 +9,11 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { injectMetaAttributes } from './inject_meta_attributes';
import { ISavedObjectsManagement } from '../services';
-import { SavedObjectRelation, SavedObjectWithMetadata } from '../types';
+import {
+ SavedObjectInvalidRelation,
+ SavedObjectWithMetadata,
+ SavedObjectGetRelationshipsResponse,
+} from '../types';
export async function findRelationships({
type,
@@ -25,17 +29,19 @@ export async function findRelationships({
client: SavedObjectsClientContract;
referenceTypes: string[];
savedObjectsManagement: ISavedObjectsManagement;
-}): Promise {
+}): Promise {
const { references = [] } = await client.get(type, id);
// Use a map to avoid duplicates, it does happen but have a different "name" in the reference
- const referencedToBulkGetOpts = new Map(
- references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
- );
+ const childrenReferences = [
+ ...new Map(
+ references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
+ ).values(),
+ ];
const [childReferencesResponse, parentReferencesResponse] = await Promise.all([
- referencedToBulkGetOpts.size > 0
- ? client.bulkGet([...referencedToBulkGetOpts.values()])
+ childrenReferences.length > 0
+ ? client.bulkGet(childrenReferences)
: Promise.resolve({ saved_objects: [] }),
client.find({
hasReference: { type, id },
@@ -44,28 +50,37 @@ export async function findRelationships({
}),
]);
- return childReferencesResponse.saved_objects
- .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
- .map(extractCommonProperties)
- .map(
- (obj) =>
- ({
- ...obj,
- relationship: 'child',
- } as SavedObjectRelation)
- )
- .concat(
- parentReferencesResponse.saved_objects
- .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
- .map(extractCommonProperties)
- .map(
- (obj) =>
- ({
- ...obj,
- relationship: 'parent',
- } as SavedObjectRelation)
- )
- );
+ const invalidRelations: SavedObjectInvalidRelation[] = childReferencesResponse.saved_objects
+ .filter((obj) => Boolean(obj.error))
+ .map((obj) => ({
+ id: obj.id,
+ type: obj.type,
+ relationship: 'child',
+ error: obj.error!.message,
+ }));
+
+ const relations = [
+ ...childReferencesResponse.saved_objects
+ .filter((obj) => !obj.error)
+ .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
+ .map(extractCommonProperties)
+ .map((obj) => ({
+ ...obj,
+ relationship: 'child' as const,
+ })),
+ ...parentReferencesResponse.saved_objects
+ .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
+ .map(extractCommonProperties)
+ .map((obj) => ({
+ ...obj,
+ relationship: 'parent' as const,
+ })),
+ ];
+
+ return {
+ relations,
+ invalidRelations,
+ };
}
function extractCommonProperties(savedObject: SavedObjectWithMetadata) {
diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts
index 3a52c973fde8d..5417ff2926120 100644
--- a/src/plugins/saved_objects_management/server/routes/relationships.ts
+++ b/src/plugins/saved_objects_management/server/routes/relationships.ts
@@ -38,7 +38,7 @@ export const registerRelationshipsRoute = (
? req.query.savedObjectTypes
: [req.query.savedObjectTypes];
- const relations = await findRelationships({
+ const findRelationsResponse = await findRelationships({
type,
id,
client,
@@ -48,7 +48,7 @@ export const registerRelationshipsRoute = (
});
return res.ok({
- body: relations,
+ body: findRelationsResponse,
});
})
);
diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts
index 710bb5db7d1cb..562970d2d2dcd 100644
--- a/src/plugins/saved_objects_management/server/types.ts
+++ b/src/plugins/saved_objects_management/server/types.ts
@@ -12,4 +12,11 @@ export interface SavedObjectsManagementPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SavedObjectsManagementPluginStart {}
-export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
+export {
+ SavedObjectMetadata,
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../common';
diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts
index 185c6ded01de4..6dea461f790e8 100644
--- a/test/api_integration/apis/saved_objects_management/relationships.ts
+++ b/test/api_integration/apis/saved_objects_management/relationships.ts
@@ -14,23 +14,32 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
- const responseSchema = schema.arrayOf(
- schema.object({
- id: schema.string(),
- type: schema.string(),
- relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
- meta: schema.object({
- title: schema.string(),
- icon: schema.string(),
- editUrl: schema.string(),
- inAppUrl: schema.object({
- path: schema.string(),
- uiCapabilitiesPath: schema.string(),
- }),
- namespaceType: schema.string(),
+ const relationSchema = schema.object({
+ id: schema.string(),
+ type: schema.string(),
+ relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
+ meta: schema.object({
+ title: schema.string(),
+ icon: schema.string(),
+ editUrl: schema.string(),
+ inAppUrl: schema.object({
+ path: schema.string(),
+ uiCapabilitiesPath: schema.string(),
}),
- })
- );
+ namespaceType: schema.string(),
+ }),
+ });
+ const invalidRelationSchema = schema.object({
+ id: schema.string(),
+ type: schema.string(),
+ relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
+ error: schema.string(),
+ });
+
+ const responseSchema = schema.object({
+ relations: schema.arrayOf(relationSchema),
+ invalidRelations: schema.arrayOf(invalidRelationSchema),
+ });
describe('relationships', () => {
before(async () => {
@@ -64,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@@ -108,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@@ -145,8 +154,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
- // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
- it.skip('should return 404 if search finds no results', async () => {
+ it('should return 404 if search finds no results', async () => {
await supertest
.get(relationshipsUrl('search', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@@ -169,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@@ -210,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357', ['search']))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@@ -246,8 +254,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
- // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
- it.skip('should return 404 if dashboard finds no results', async () => {
+ it('should return 404 if dashboard finds no results', async () => {
await supertest
.get(relationshipsUrl('dashboard', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@@ -270,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -313,7 +320,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -356,7 +363,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -399,7 +406,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -425,5 +432,48 @@ export default function ({ getService }: FtrProviderContext) {
.expect(404);
});
});
+
+ describe('invalid references', () => {
+ it('should validate the response schema', async () => {
+ const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
+
+ expect(() => {
+ responseSchema.validate(resp.body);
+ }).not.to.throwError();
+ });
+
+ it('should return the invalid relations', async () => {
+ const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
+
+ expect(resp.body).to.eql({
+ invalidRelations: [
+ {
+ error: 'Saved object [visualization/invalid-vis] not found',
+ id: 'invalid-vis',
+ relationship: 'child',
+ type: 'visualization',
+ },
+ ],
+ relations: [
+ {
+ id: 'add810b0-3224-11e8-a572-ffca06da1357',
+ meta: {
+ editUrl:
+ '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ namespaceType: 'single',
+ title: 'Visualization',
+ },
+ relationship: 'child',
+ type: 'visualization',
+ },
+ ],
+ });
+ });
+ });
});
}
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
new file mode 100644
index 0000000000000..21d84c4b55e55
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
@@ -0,0 +1,190 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "timelion-sheet:190f3e90-2ec3-11e8-ba48-69fc4e41e1f6",
+ "source": {
+ "type": "timelion-sheet",
+ "updated_at": "2018-03-23T17:53:30.872Z",
+ "timelion-sheet": {
+ "title": "New TimeLion Sheet",
+ "hits": 0,
+ "description": "",
+ "timelion_sheet": [
+ ".es(*)"
+ ],
+ "timelion_interval": "auto",
+ "timelion_chart_height": 275,
+ "timelion_columns": 2,
+ "timelion_rows": 2,
+ "version": 1
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "index-pattern",
+ "updated_at": "2018-03-28T01:08:34.290Z",
+ "index-pattern": {
+ "title": "saved_objects*",
+ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "config:7.0.0-alpha1",
+ "source": {
+ "type": "config",
+ "updated_at": "2018-03-28T01:08:39.248Z",
+ "config": {
+ "buildNum": 8467,
+ "telemetry:optIn": false,
+ "defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "search:960372e0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "search",
+ "updated_at": "2018-03-28T01:08:55.182Z",
+ "search": {
+ "title": "OneRecord",
+ "description": "",
+ "hits": 0,
+ "columns": [
+ "_source"
+ ],
+ "sort": [
+ "_score",
+ "desc"
+ ],
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2018-03-28T01:09:18.936Z",
+ "visualization": {
+ "title": "VisualizationFromSavedSearch",
+ "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
+ "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
+ "description": "",
+ "savedSearchId": "960372e0-3224-11e8-a572-ffca06da1357",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:add810b0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2018-03-28T01:09:35.163Z",
+ "visualization": {
+ "title": "Visualization",
+ "visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
+ "uiStateJSON": "{}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "dashboard",
+ "updated_at": "2018-03-28T01:09:50.606Z",
+ "dashboard": {
+ "title": "Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"add810b0-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"a42c0580-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}}]",
+ "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "dashboard:invalid-refs",
+ "source": {
+ "type": "dashboard",
+ "updated_at": "2018-03-28T01:09:50.606Z",
+ "dashboard": {
+ "title": "Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[]",
+ "optionsJSON": "{}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{}"
+ }
+ },
+ "references": [
+ {
+ "type":"visualization",
+ "id": "add810b0-3224-11e8-a572-ffca06da1357",
+ "name": "valid-ref"
+ },
+ {
+ "type":"visualization",
+ "id": "invalid-vis",
+ "name": "missing-ref"
+ }
+ ]
+ }
+ }
+}
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz
deleted file mode 100644
index 0834567abb66b663079894089ed4edd91f1cf0b3..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1385
zcmV-v1(y0BiwFP!000026V+JVZ`(Eyf6rfGXfIn48~X5vthb^?hYW2R#6}+$2LUCX
zWv;U5QB=~@(Eq+8C0mrECGvtGnI97Qcs$m8BGZD22gy7Lt@`B_*dyDA^hk#?yYb0+4|-wU-`D?Y;|<*LNK7`ym-mNf3G{|YrRCa=-?zQK>&=}>F!BP=9{3aY&szV$
zPJNPIlZig;9PWB^RQ!yJy;cWFiU@hL0v4~-Deh#{s>PFhohtX;wq?QZ4%co$WMx=R
zB`i*Me~Xji3UJ=YzUfFYxa+g~mtVvi|tywT)oz%*<=
zi5GuvJAv&7-f-YfZ38b&GwpE6$Sqpr;a?ER?46mNC4+>j8?~;s3o9jSSXjZrx?yx-
zoi4PmT98S>(pbwPo~IIpHa?f20#pu`B*{RDfCx-=n5d0XmXn12Br5R%8M=`@
z@}CLbhRu!`o(7ITn0jLa!%Z{oQ2u7>C=%5$m^G`Xv^A4>dX;v)U*G*>2Ab4gu{Me}
zM38k>=5_<(qRgYC8^Ma-UEr+BNOFnerr8g01%b(;?7c$HXSju=v5wVInk;MUtU_j*
zCkZZ7CJ@;r!j!0}OwPF^iD5>n@1OECDm!PsE@6e8M;)dHHPzB^$?-
z?M*kg6|9LCDn4e>!6g(0Le;qIoaw7Jstj+xx-H}8jtv+;9r-G&Q+TF9egr4K5YHH8
z{cqgx2rs+_6Hw|qcK9kx;9)l#d(UBl<4eC;>#aD)Dx!4Gc_P`ynCU3}3^AnU?AK;z
z_gIkzW>#XJzi?{K$aw~rhyXB&0jqKX
zJa<^$+w06QBYQBm%~_*1(atU(9~^P?Z)F>jVs-73tAHE}MnB@)V3{9PZthSGn5rm8
z`0%4D)BEZ_+u^=w44ezge2uHHjc1+h!Q(X9?e+ojRVCGh^vkNlw_r+D<$ciabY&Ht
zc8p0&nnAh82jzARs>4kCNKn^i4!O>3W>hF8;`(&bg?DMtAR@76`}lotR1KQp@|
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
index c670508247b1a..6dd4d198e0f67 100644
--- a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
+++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
@@ -12,6 +12,20 @@
"mappings": {
"dynamic": "strict",
"properties": {
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
"config": {
"dynamic": "true",
"properties": {
@@ -280,4 +294,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts
index 9491661de73ef..5e4eaefb7e9d1 100644
--- a/test/functional/apps/saved_objects_management/index.ts
+++ b/test/functional/apps/saved_objects_management/index.ts
@@ -12,5 +12,6 @@ export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderC
describe('saved objects management', function savedObjectsManagementAppTestSuite() {
this.tags('ciGroup7');
loadTestFile(require.resolve('./edit_saved_object'));
+ loadTestFile(require.resolve('./show_relationships'));
});
}
diff --git a/test/functional/apps/saved_objects_management/show_relationships.ts b/test/functional/apps/saved_objects_management/show_relationships.ts
new file mode 100644
index 0000000000000..6f3fb5a4973e2
--- /dev/null
+++ b/test/functional/apps/saved_objects_management/show_relationships.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']);
+
+ describe('saved objects relationships flyout', () => {
+ beforeEach(async () => {
+ await esArchiver.load('saved_objects_management/show_relationships');
+ });
+
+ afterEach(async () => {
+ await esArchiver.unload('saved_objects_management/show_relationships');
+ });
+
+ it('displays the invalid references', async () => {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+
+ const objects = await PageObjects.savedObjects.getRowTitles();
+ expect(objects.includes('Dashboard with missing refs')).to.be(true);
+
+ await PageObjects.savedObjects.clickRelationshipsByTitle('Dashboard with missing refs');
+
+ const invalidRelations = await PageObjects.savedObjects.getInvalidRelations();
+
+ expect(invalidRelations).to.eql([
+ {
+ error: 'Saved object [visualization/missing-vis-ref] not found',
+ id: 'missing-vis-ref',
+ relationship: 'Child',
+ type: 'visualization',
+ },
+ {
+ error: 'Saved object [dashboard/missing-dashboard-ref] not found',
+ id: 'missing-dashboard-ref',
+ relationship: 'Child',
+ type: 'dashboard',
+ },
+ ]);
+ });
+ });
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
new file mode 100644
index 0000000000000..4d5b969a3c931
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
@@ -0,0 +1,36 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "dashboard:dash-with-missing-refs",
+ "source": {
+ "dashboard": {
+ "title": "Dashboard with missing refs",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[]",
+ "optionsJSON": "{}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{}"
+ }
+ },
+ "type": "dashboard",
+ "references": [
+ {
+ "type": "visualization",
+ "id": "missing-vis-ref",
+ "name": "some missing ref"
+ },
+ {
+ "type": "dashboard",
+ "id": "missing-dashboard-ref",
+ "name": "some other missing ref"
+ }
+ ],
+ "updated_at": "2019-01-22T19:32:47.232Z"
+ }
+ }
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
new file mode 100644
index 0000000000000..d53e6c96e883e
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
@@ -0,0 +1,473 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "auto_expand_replicas": "0-1",
+ "number_of_replicas": "0"
+ }
+ },
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "apm-telemetry": {
+ "properties": {
+ "has_any_services": {
+ "type": "boolean"
+ },
+ "services_per_agent": {
+ "properties": {
+ "go": {
+ "type": "long",
+ "null_value": 0
+ },
+ "java": {
+ "type": "long",
+ "null_value": 0
+ },
+ "js-base": {
+ "type": "long",
+ "null_value": 0
+ },
+ "nodejs": {
+ "type": "long",
+ "null_value": 0
+ },
+ "python": {
+ "type": "long",
+ "null_value": 0
+ },
+ "ruby": {
+ "type": "long",
+ "null_value": 0
+ }
+ }
+ }
+ }
+ },
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "id": {
+ "type": "text",
+ "index": false
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "accessibility:disableAnimations": {
+ "type": "boolean"
+ },
+ "buildNum": {
+ "type": "keyword"
+ },
+ "dateFormat:tz": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "defaultIndex": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "telemetry:optIn": {
+ "type": "boolean"
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "map": {
+ "properties": {
+ "bounds": {
+ "type": "geo_shape",
+ "tree": "quadtree"
+ },
+ "description": {
+ "type": "text"
+ },
+ "layerListJSON": {
+ "type": "text"
+ },
+ "mapStateJSON": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "typeMeta": {
+ "type": "keyword"
+ }
+ }
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "properties": {
+ "index-pattern": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "space": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "spaceId": {
+ "type": "keyword"
+ },
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index 1cdf76ad58ef0..cf162f12df9d9 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -257,6 +257,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
});
}
+ async getInvalidRelations() {
+ const rows = await testSubjects.findAll('invalidRelationshipsTableRow');
+ return mapAsync(rows, async (row) => {
+ const objectType = await row.findByTestSubject('relationshipsObjectType');
+ const objectId = await row.findByTestSubject('relationshipsObjectId');
+ const relationship = await row.findByTestSubject('directRelationship');
+ const error = await row.findByTestSubject('relationshipsError');
+ return {
+ type: await objectType.getVisibleText(),
+ id: await objectId.getVisibleText(),
+ relationship: await relationship.getVisibleText(),
+ error: await error.getVisibleText(),
+ };
+ });
+ }
+
async getTableSummary() {
const table = await testSubjects.find('savedObjectsTable');
const $ = await table.parseDomContent();
From 61a51b568481abfba41f71781d24acfd4f65c7ee Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 1 Feb 2021 11:14:46 +0100
Subject: [PATCH 13/18] [ILM] New copy for rollover and small refactor for
timeline (#89422)
* refactor timeline and relative ms calculation logic for easier use outside of edit_policy section
* further refactor, move child component to own file in timeline, and clean up public API for relative timing calculation
* added copy to call out variation in timing (slop) introduced by rollover
* use separate copy for timeline
* remove unused import
* fix unresolved merge
* implement copy feedback
* added component integration for showing/hiding hot phase icon on timeline
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../edit_policy/edit_policy.helpers.tsx | 1 +
.../edit_policy/edit_policy.test.ts | 8 +
.../components/phases/hot_phase/hot_phase.tsx | 5 +
.../components/timeline/components/index.ts | 7 +
.../components/timeline_phase_text.tsx | 28 ++
.../edit_policy/components/timeline/index.ts | 2 +-
.../timeline/timeline.container.tsx | 33 +++
.../components/timeline/timeline.scss | 4 +
.../components/timeline/timeline.tsx | 252 ++++++++++--------
.../sections/edit_policy/i18n_texts.ts | 7 +
...absolute_timing_to_relative_timing.test.ts | 9 +-
.../lib/absolute_timing_to_relative_timing.ts | 78 +++---
.../sections/edit_policy/lib/index.ts | 5 +-
13 files changed, 288 insertions(+), 151 deletions(-)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index 64b654b030236..d9256ec916ec8 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -251,6 +251,7 @@ export const setup = async (arg?: { appServicesContext: Partial exists('timelineHotPhaseRolloverToolTip'),
hasHotPhase: () => exists('ilmTimelineHotPhase'),
hasWarmPhase: () => exists('ilmTimelineWarmPhase'),
hasColdPhase: () => exists('ilmTimelineColdPhase'),
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index bb96e8b4df239..05793a4bed581 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -843,5 +843,13 @@ describe('', () => {
expect(actions.timeline.hasColdPhase()).toBe(true);
expect(actions.timeline.hasDeletePhase()).toBe(true);
});
+
+ test('show and hide rollover indicator on timeline', async () => {
+ const { actions } = testBed;
+ expect(actions.timeline.hasRolloverIndicator()).toBe(true);
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ expect(actions.timeline.hasRolloverIndicator()).toBe(false);
+ });
});
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index fb7c9a80acba0..02de47f8c56ef 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -16,6 +16,7 @@ import {
EuiTextColor,
EuiSwitch,
EuiIconTip,
+ EuiIcon,
} from '@elastic/eui';
import { useFormData, UseField, SelectField, NumericField } from '../../../../../../shared_imports';
@@ -80,6 +81,10 @@ export const HotPhase: FunctionComponent = () => {
+
+
+ {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}
+
path={isUsingDefaultRolloverPath}>
{(field) => (
<>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
new file mode 100644
index 0000000000000..1c9d5e1abc316
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TimelinePhaseText } from './timeline_phase_text';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
new file mode 100644
index 0000000000000..a44e0f2407c52
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent, ReactNode } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+
+export const TimelinePhaseText: FunctionComponent<{
+ phaseName: ReactNode | string;
+ durationInPhase?: ReactNode | string;
+}> = ({ phaseName, durationInPhase }) => (
+
+
+
+ {phaseName}
+
+
+
+ {typeof durationInPhase === 'string' ? (
+ {durationInPhase}
+ ) : (
+ durationInPhase
+ )}
+
+
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
index 4664429db37d7..7bcaa6584edf0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
@@ -3,4 +3,4 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-export { Timeline } from './timeline';
+export { Timeline } from './timeline.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx
new file mode 100644
index 0000000000000..75f53fcb25091
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+
+import { useFormData } from '../../../../../shared_imports';
+
+import { formDataToAbsoluteTimings } from '../../lib';
+
+import { useConfigurationIssues } from '../../form';
+
+import { FormInternal } from '../../types';
+
+import { Timeline as ViewComponent } from './timeline';
+
+export const Timeline: FunctionComponent = () => {
+ const [formData] = useFormData();
+ const timings = formDataToAbsoluteTimings(formData);
+ const { isUsingRollover } = useConfigurationIssues();
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
index 452221a29a991..7d65d2cd6b212 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
@@ -84,4 +84,8 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%);
background-color: $euiColorVis1;
}
}
+
+ &__rolloverIcon {
+ display: inline-block;
+ }
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
index 40bab9c676de2..2e2db88e1384d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
-import React, { FunctionComponent, useMemo } from 'react';
+import React, { FunctionComponent, memo } from 'react';
import {
- EuiText,
EuiIcon,
EuiIconProps,
EuiFlexGroup,
@@ -16,18 +15,19 @@ import {
} from '@elastic/eui';
import { PhasesExceptDelete } from '../../../../../../common/types';
-import { useFormData } from '../../../../../shared_imports';
-
-import { FormInternal } from '../../types';
import {
- calculateRelativeTimingMs,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable,
PhaseAgeInMilliseconds,
+ AbsoluteTimings,
} from '../../lib';
import './timeline.scss';
import { InfinityIconSvg } from './infinity_icon.svg';
+import { TimelinePhaseText } from './components';
+
+const exists = (v: unknown) => v != null;
const InfinityIcon: FunctionComponent> = (props) => (
@@ -56,6 +56,13 @@ const i18nTexts = {
hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', {
defaultMessage: 'Hot phase',
}),
+ rolloverTooltip: i18n.translate(
+ 'xpack.indexLifecycleMgmt.timeline.hotPhaseRolloverToolTipContent',
+ {
+ defaultMessage:
+ 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.',
+ }
+ ),
warmPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle', {
defaultMessage: 'Warm phase',
}),
@@ -88,121 +95,136 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => {
};
};
-const TimelinePhaseText: FunctionComponent<{
- phaseName: string;
- durationInPhase?: React.ReactNode | string;
-}> = ({ phaseName, durationInPhase }) => (
-
-
-
- {phaseName}
-
-
-
- {typeof durationInPhase === 'string' ? (
- {durationInPhase}
- ) : (
- durationInPhase
- )}
-
-
-);
-
-export const Timeline: FunctionComponent = () => {
- const [formData] = useFormData();
-
- const phaseTimingInMs = useMemo(() => {
- return calculateRelativeTimingMs(formData);
- }, [formData]);
+interface Props {
+ hasDeletePhase: boolean;
+ /**
+ * For now we assume the hot phase does not have a min age
+ */
+ hotPhaseMinAge: undefined;
+ isUsingRollover: boolean;
+ warmPhaseMinAge?: string;
+ coldPhaseMinAge?: string;
+ deletePhaseMinAge?: string;
+}
- const humanReadableTimings = useMemo(() => normalizeTimingsToHumanReadable(phaseTimingInMs), [
- phaseTimingInMs,
- ]);
-
- const widths = calculateWidths(phaseTimingInMs);
-
- const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode =>
- phaseTimingInMs.phases[phase] === Infinity ? (
-
- ) : (
- humanReadableTimings[phase]
- );
-
- return (
-
-
-
-
- {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
- defaultMessage: 'Policy Timeline',
- })}
-
-
-
-
- {
- if (el) {
- el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot);
- el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null);
- el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null);
- }
- }}
- >
-
-
-
- {/* These are the actual color bars for the timeline */}
-
- {formData._meta?.warm.enabled && (
+/**
+ * Display a timeline given ILM policy phase information. This component is re-usable and memo-ized
+ * and should not rely directly on any application-specific context.
+ */
+export const Timeline: FunctionComponent
= memo(
+ ({ hasDeletePhase, isUsingRollover, ...phasesMinAge }) => {
+ const absoluteTimings: AbsoluteTimings = {
+ hot: { min_age: phasesMinAge.hotPhaseMinAge },
+ warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined,
+ cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined,
+ delete: phasesMinAge.deletePhaseMinAge
+ ? { min_age: phasesMinAge.deletePhaseMinAge }
+ : undefined,
+ };
+
+ const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings);
+ const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds);
+
+ const widths = calculateWidths(phaseAgeInMilliseconds);
+
+ const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode =>
+ phaseAgeInMilliseconds.phases[phase] === Infinity ? (
+
+ ) : (
+ humanReadableTimings[phase]
+ );
+
+ return (
+
+
+
+
+ {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
+ defaultMessage: 'Policy Timeline',
+ })}
+
+
+
+
+ {
+ if (el) {
+ el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot);
+ el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null);
+ el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null);
+ }
+ }}
+ >
+
+
+
+ {/* These are the actual color bars for the timeline */}
-
+
+ {i18nTexts.hotPhase}
+
+
+
+
+ >
+ ) : (
+ i18nTexts.hotPhase
+ )
+ }
+ durationInPhase={getDurationInPhaseContent('hot')}
/>
- )}
- {formData._meta?.cold.enabled && (
+ {exists(phaseAgeInMilliseconds.phases.warm) && (
+
+ )}
+ {exists(phaseAgeInMilliseconds.phases.cold) && (
+
+ )}
+
+
+ {hasDeletePhase && (
+
- )}
-
-
- {formData._meta?.delete.enabled && (
-
-
-
-
-
- )}
-
-
-
-
- );
-};
+
+ )}
+
+
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 71085a6d7a2b8..cf8c92b8333d0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -11,6 +11,13 @@ export const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.indexFieldLabel', {
defaultMessage: 'Shrink index',
}),
+ rolloverOffsetsHotPhaseTiming: i18n.translate(
+ 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription',
+ {
+ defaultMessage:
+ 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.',
+ }
+ ),
searchableSnapshotInHotPhase: {
searchableSnapshotDisallowed: {
calloutTitle: i18n.translate(
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
index 28910871fa33b..405de2b55a2f7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
@@ -4,13 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { flow } from 'fp-ts/function';
import { deserializer } from '../form';
import {
+ formDataToAbsoluteTimings,
+ calculateRelativeFromAbsoluteMilliseconds,
absoluteTimingToRelativeTiming,
- calculateRelativeTimingMs,
} from './absolute_timing_to_relative_timing';
+export const calculateRelativeTimingMs = flow(
+ formDataToAbsoluteTimings,
+ calculateRelativeFromAbsoluteMilliseconds
+);
+
describe('Conversion of absolute policy timing to relative timing', () => {
describe('calculateRelativeTimingMs', () => {
describe('policy that never deletes data (keep forever)', () => {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
index 2f37608b2d7ae..a44863b2f1ce2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
@@ -14,16 +14,21 @@
*
* This code converts the absolute timings to _relative_ timings of the form: 30 days in hot phase,
* 40 days in warm phase then forever in cold phase.
+ *
+ * All functions exported from this file can be viewed as utilities for working with form data and
+ * other defined interfaces to calculate the relative amount of time data will spend in a phase.
*/
import moment from 'moment';
-import { flow } from 'fp-ts/lib/function';
import { i18n } from '@kbn/i18n';
+import { flow } from 'fp-ts/function';
import { splitSizeAndUnits } from '../../../lib/policies';
import { FormInternal } from '../types';
+/* -===- Private functions and types -===- */
+
type MinAgePhase = 'warm' | 'cold' | 'delete';
type Phase = 'hot' | MinAgePhase;
@@ -43,7 +48,34 @@ const i18nTexts = {
}),
};
-interface AbsoluteTimings {
+const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
+
+const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
+ min_age: formData.phases?.[phase]?.min_age
+ ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit
+ : '0ms',
+});
+
+/**
+ * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
+ * for all date math values. ILM policies also support "micros" and "nanos".
+ */
+const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
+ let milliseconds: number;
+ const { units, size } = splitSizeAndUnits(phase.min_age);
+ if (units === 'micros') {
+ milliseconds = parseInt(size, 10) / 1e3;
+ } else if (units === 'nanos') {
+ milliseconds = parseInt(size, 10) / 1e6;
+ } else {
+ milliseconds = moment.duration(size, units as any).asMilliseconds();
+ }
+ return milliseconds;
+};
+
+/* -===- Public functions and types -===- */
+
+export interface AbsoluteTimings {
hot: {
min_age: undefined;
};
@@ -67,16 +99,7 @@ export interface PhaseAgeInMilliseconds {
};
}
-const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
-
-const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
- min_age:
- formData.phases && formData.phases[phase]?.min_age
- ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit
- : '0ms',
-});
-
-const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
+export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
const { _meta } = formData;
if (!_meta) {
return { hot: { min_age: undefined } };
@@ -89,28 +112,13 @@ const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
};
};
-/**
- * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
- * for all date math values. ILM policies also support "micros" and "nanos".
- */
-const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
- let milliseconds: number;
- const { units, size } = splitSizeAndUnits(phase.min_age);
- if (units === 'micros') {
- milliseconds = parseInt(size, 10) / 1e3;
- } else if (units === 'nanos') {
- milliseconds = parseInt(size, 10) / 1e6;
- } else {
- milliseconds = moment.duration(size, units as any).asMilliseconds();
- }
- return milliseconds;
-};
-
/**
* Given a set of phase minimum age absolute timings, like hot phase 0ms and warm phase 3d, work out
* the number of milliseconds data will reside in phase.
*/
-const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds => {
+export const calculateRelativeFromAbsoluteMilliseconds = (
+ inputs: AbsoluteTimings
+): PhaseAgeInMilliseconds => {
return phaseOrder.reduce(
(acc, phaseName, idx) => {
// Delete does not have an age associated with it
@@ -152,6 +160,8 @@ const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds
);
};
+export type RelativePhaseTimingInMs = ReturnType;
+
const millisecondsToDays = (milliseconds?: number): string | undefined => {
if (milliseconds == null) {
return;
@@ -177,10 +187,12 @@ export const normalizeTimingsToHumanReadable = ({
};
};
-export const calculateRelativeTimingMs = flow(formDataToAbsoluteTimings, calculateMilliseconds);
-
+/**
+ * Given {@link FormInternal}, extract the min_age values for each phase and calculate
+ * human readable strings for communicating how long data will remain in a phase.
+ */
export const absoluteTimingToRelativeTiming = flow(
formDataToAbsoluteTimings,
- calculateMilliseconds,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable
);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
index 9593fcc810a6f..a9372c99a72fc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
@@ -6,7 +6,10 @@
export {
absoluteTimingToRelativeTiming,
- calculateRelativeTimingMs,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable,
+ formDataToAbsoluteTimings,
+ AbsoluteTimings,
PhaseAgeInMilliseconds,
+ RelativePhaseTimingInMs,
} from './absolute_timing_to_relative_timing';
From 19b1f46611d05a9e494b1fe107bf103d417a0456 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 1 Feb 2021 12:43:06 +0200
Subject: [PATCH 14/18] Fixes flakiness on timelion suggestions (#89538)
* Fixes flakiness on timelion suggestions
* Improvements
* Remove flakiness
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
test/functional/apps/timelion/_expression_typeahead.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js
index 744f8de15e767..3db5cb48dd38b 100644
--- a/test/functional/apps/timelion/_expression_typeahead.js
+++ b/test/functional/apps/timelion/_expression_typeahead.js
@@ -75,18 +75,18 @@ export default function ({ getPageObjects }) {
await PageObjects.timelion.updateExpression(',split');
await PageObjects.timelion.clickSuggestion();
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
- expect(suggestions.length).to.eql(51);
+ expect(suggestions.length).not.to.eql(0);
expect(suggestions[0].includes('@message.raw')).to.eql(true);
- await PageObjects.timelion.clickSuggestion(10, 2000);
+ await PageObjects.timelion.clickSuggestion(10);
});
it('should show field suggestions for metric argument when index pattern set', async () => {
await PageObjects.timelion.updateExpression(',metric');
await PageObjects.timelion.clickSuggestion();
await PageObjects.timelion.updateExpression('avg:');
- await PageObjects.timelion.clickSuggestion(0, 2000);
+ await PageObjects.timelion.clickSuggestion(0);
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
- expect(suggestions.length).to.eql(2);
+ expect(suggestions.length).not.to.eql(0);
expect(suggestions[0].includes('avg:bytes')).to.eql(true);
});
});
From e31b6a8c91e88741238bddc90147a825444640eb Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 1 Feb 2021 11:52:57 +0100
Subject: [PATCH 15/18] [Lens] Add smoke test for lens in canvas (#88657)
---
x-pack/test/functional/apps/canvas/index.js | 1 +
x-pack/test/functional/apps/canvas/lens.ts | 30 ++
x-pack/test/functional/config.js | 1 +
.../es_archives/canvas/lens/data.json | 190 ++++++++
.../es_archives/canvas/lens/mappings.json | 409 ++++++++++++++++++
5 files changed, 631 insertions(+)
create mode 100644 x-pack/test/functional/apps/canvas/lens.ts
create mode 100644 x-pack/test/functional/es_archives/canvas/lens/data.json
create mode 100644 x-pack/test/functional/es_archives/canvas/lens/mappings.json
diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js
index b7031cf0e55da..d5f7540f48c83 100644
--- a/x-pack/test/functional/apps/canvas/index.js
+++ b/x-pack/test/functional/apps/canvas/index.js
@@ -26,6 +26,7 @@ export default function canvasApp({ loadTestFile, getService }) {
loadTestFile(require.resolve('./custom_elements'));
loadTestFile(require.resolve('./feature_controls/canvas_security'));
loadTestFile(require.resolve('./feature_controls/canvas_spaces'));
+ loadTestFile(require.resolve('./lens'));
loadTestFile(require.resolve('./reports'));
});
}
diff --git a/x-pack/test/functional/apps/canvas/lens.ts b/x-pack/test/functional/apps/canvas/lens.ts
new file mode 100644
index 0000000000000..e74795de6c7ea
--- /dev/null
+++ b/x-pack/test/functional/apps/canvas/lens.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function canvasLensTest({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['canvas', 'common', 'header', 'lens']);
+ const esArchiver = getService('esArchiver');
+
+ describe('lens in canvas', function () {
+ before(async () => {
+ await esArchiver.load('canvas/lens');
+ // open canvas home
+ await PageObjects.common.navigateToApp('canvas');
+ // load test workpad
+ await PageObjects.common.navigateToApp('canvas', {
+ hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1',
+ });
+ });
+
+ it('renders lens visualization', async () => {
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await PageObjects.lens.assertMetric('Maximum of bytes', '16,788');
+ });
+ });
+}
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index 1815942a06a9a..fc508f8477ebe 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -232,6 +232,7 @@ export default async function ({ readConfigFile }) {
{
feature: {
canvas: ['all'],
+ visualize: ['all'],
},
spaces: ['*'],
},
diff --git a/x-pack/test/functional/es_archives/canvas/lens/data.json b/x-pack/test/functional/es_archives/canvas/lens/data.json
new file mode 100644
index 0000000000000..dca7d31d71082
--- /dev/null
+++ b/x-pack/test/functional/es_archives/canvas/lens/data.json
@@ -0,0 +1,190 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "space:default",
+ "index": ".kibana_1",
+ "source": {
+ "space": {
+ "_reserved": true,
+ "color": "#00bfb3",
+ "description": "This is your default space!",
+ "name": "Default"
+ },
+ "type": "space",
+ "updated_at": "2018-11-06T18:20:26.703Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "canvas-workpad:workpad-1705f884-6224-47de-ba49-ca224fe6ec31",
+ "index": ".kibana_1",
+ "source": {
+ "canvas-workpad": {
+ "@created": "2018-11-19T19:17:12.646Z",
+ "@timestamp": "2018-11-19T19:36:28.499Z",
+ "assets": {
+ },
+ "colors": [
+ "#37988d",
+ "#c19628",
+ "#b83c6f",
+ "#3f9939",
+ "#1785b0",
+ "#ca5f35",
+ "#45bdb0",
+ "#f2bc33",
+ "#e74b8b",
+ "#4fbf48",
+ "#1ea6dc",
+ "#fd7643",
+ "#72cec3",
+ "#f5cc5d",
+ "#ec77a8",
+ "#7acf74",
+ "#4cbce4",
+ "#fd986f",
+ "#a1ded7",
+ "#f8dd91",
+ "#f2a4c5",
+ "#a6dfa2",
+ "#86d2ed",
+ "#fdba9f",
+ "#000000",
+ "#444444",
+ "#777777",
+ "#BBBBBB",
+ "#FFFFFF",
+ "rgba(255,255,255,0)"
+ ],
+ "height": 920,
+ "id": "workpad-1705f884-6224-47de-ba49-ca224fe6ec31",
+ "isWriteable": true,
+ "name": "Test Workpad",
+ "page": 0,
+ "pages": [
+ {
+ "elements": [
+ {
+ "expression": "savedLens id=\"my-lens-vis\" timerange={timerange from=\"2014-01-01\" to=\"2018-01-01\"}",
+ "id": "element-8f64a10a-01f3-4a71-a682-5b627cbe4d0e",
+ "position": {
+ "angle": 0,
+ "height": 238,
+ "left": 33.5,
+ "top": 20,
+ "width": 338
+ }
+ }
+ ],
+ "id": "page-c38cd459-10fe-45f9-847b-2cbd7ec74319",
+ "style": {
+ "background": "#fff"
+ },
+ "transition": {
+ }
+ }
+ ],
+ "width": 840
+ },
+ "type": "canvas-workpad",
+ "updated_at": "2018-11-19T19:36:28.511Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "lens:my-lens-vis",
+ "index": ".kibana_1",
+ "source": {
+ "lens": {
+ "expression": "",
+ "state": {
+ "datasourceMetaData": {
+ "filterableIndexPatterns": [
+ {
+ "id": "logstash-lens",
+ "title": "logstash-lens"
+ }
+ ]
+ },
+ "datasourceStates": {
+ "indexpattern": {
+ "currentIndexPatternId": "logstash-lens",
+ "layers": {
+ "c61a8afb-a185-4fae-a064-fb3846f6c451": {
+ "columnOrder": [
+ "2cd09808-3915-49f4-b3b0-82767eba23f7"
+ ],
+ "columns": {
+ "2cd09808-3915-49f4-b3b0-82767eba23f7": {
+ "dataType": "number",
+ "isBucketed": false,
+ "label": "Maximum of bytes",
+ "operationType": "max",
+ "scale": "ratio",
+ "sourceField": "bytes"
+ }
+ },
+ "indexPatternId": "logstash-lens"
+ }
+ }
+ }
+ },
+ "filters": [],
+ "query": {
+ "language": "kuery",
+ "query": ""
+ },
+ "visualization": {
+ "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7",
+ "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451"
+ }
+ },
+ "title": "Artistpreviouslyknownaslens",
+ "visualizationType": "lnsMetric"
+ },
+ "references": [],
+ "type": "lens",
+ "updated_at": "2019-10-16T00:28:08.979Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": "logstash-lens",
+ "id": "1",
+ "source": {
+ "@timestamp": "2015-09-20T02:00:00.000Z",
+ "bytes": 16788
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:logstash-lens",
+ "index": ".kibana_1",
+ "source": {
+ "index-pattern" : {
+ "title" : "logstash-lens",
+ "timeFieldName" : "@timestamp",
+ "fields" : "[]"
+ },
+ "type" : "index-pattern",
+ "references" : [ ],
+ "migrationVersion" : {
+ "index-pattern" : "7.6.0"
+ },
+ "updated_at" : "2020-08-19T08:39:09.998Z"
+ },
+ "type": "_doc"
+ }
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/es_archives/canvas/lens/mappings.json b/x-pack/test/functional/es_archives/canvas/lens/mappings.json
new file mode 100644
index 0000000000000..811bfaaae0d2c
--- /dev/null
+++ b/x-pack/test/functional/es_archives/canvas/lens/mappings.json
@@ -0,0 +1,409 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ ".kibana": {
+ }
+ },
+ "index": ".kibana_1",
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "id": {
+ "index": false,
+ "type": "text"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ }
+ }
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "typeMeta": {
+ "type": "keyword"
+ }
+ }
+ },
+ "lens": {
+ "properties": {
+ "expression": {
+ "index": false,
+ "type": "keyword"
+ },
+ "state": {
+ "type": "flattened"
+ },
+ "title": {
+ "type": "text"
+ },
+ "visualizationType": {
+ "type": "keyword"
+ }
+ }
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "type": "object"
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_replicas": "1",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
+
+{
+ "type": "index",
+ "value": {
+ "index": "logstash-lens",
+ "mappings": {
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "bytes": {
+ "type": "float"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "number_of_replicas": "0"
+ }
+ }
+ }
+}
\ No newline at end of file
From 1b8c3c1dcc5c077a61d3c66511a525157374f4f7 Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Mon, 1 Feb 2021 11:54:16 +0100
Subject: [PATCH 16/18] [Lens] Refactor reorder drag and drop (#88578)
---
test/functional/services/common/browser.ts | 2 +-
.../__snapshots__/drag_drop.test.tsx.snap | 24 +-
.../lens/public/drag_drop/drag_drop.scss | 11 +-
.../lens/public/drag_drop/drag_drop.test.tsx | 443 ++++++---
.../lens/public/drag_drop/drag_drop.tsx | 892 +++++++++++-------
.../lens/public/drag_drop/providers.tsx | 222 ++++-
.../plugins/lens/public/drag_drop/readme.md | 4 +-
.../config_panel/config_panel.test.tsx | 8 +
.../config_panel/config_panel.tsx | 63 +-
.../config_panel/dimension_button.tsx | 66 ++
.../draggable_dimension_button.tsx | 110 +++
.../config_panel/empty_dimension_button.tsx | 97 ++
.../config_panel/layer_panel.scss | 8 +-
.../config_panel/layer_panel.test.tsx | 199 ++--
.../editor_frame/config_panel/layer_panel.tsx | 511 ++++------
.../config_panel/remove_layer_button.tsx | 60 ++
.../editor_frame/config_panel/types.ts | 26 +-
.../editor_frame/data_panel_wrapper.tsx | 6 +-
.../editor_frame/editor_frame.test.tsx | 24 +-
.../editor_frame/editor_frame.tsx | 12 +-
.../editor_frame/suggestion_helpers.ts | 4 +-
.../workspace_panel/workspace_panel.test.tsx | 12 +-
.../workspace_panel/workspace_panel.tsx | 16 +-
.../datapanel.test.tsx | 15 +-
.../indexpattern_datasource/datapanel.tsx | 27 +-
.../dimension_panel/droppable.test.ts | 7 +
.../dimension_panel/droppable.ts | 189 ++--
.../field_item.test.tsx | 2 +
.../indexpattern_datasource/field_item.tsx | 18 +-
.../indexpattern_datasource/field_list.tsx | 11 +-
.../fields_accordion.test.tsx | 1 +
.../fields_accordion.tsx | 105 ++-
.../indexpattern_datasource/indexpattern.tsx | 4 +-
.../public/indexpattern_datasource/mocks.ts | 5 +
x-pack/plugins/lens/public/types.ts | 8 +-
.../xy_visualization/xy_config_panel.tsx | 10 +-
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../test/functional/page_objects/lens_page.ts | 2 +-
39 files changed, 2074 insertions(+), 1152 deletions(-)
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx
diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts
index 635fde6dad720..4a7e82d5b42c0 100644
--- a/test/functional/services/common/browser.ts
+++ b/test/functional/services/common/browser.ts
@@ -289,13 +289,13 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
}
const origin = document.querySelector(arguments[0]);
- const target = document.querySelector(arguments[1]);
const dragStartEvent = createEvent('dragstart');
dispatchEvent(origin, dragStartEvent);
setTimeout(() => {
const dropEvent = createEvent('drop');
+ const target = document.querySelector(arguments[1]);
dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer);
const dragEndEvent = createEvent('dragend');
dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer);
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index dc53f3a2bc2a7..6423a9f6190a7 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -13,10 +13,8 @@ exports[`DragDrop items that have droppable=false get special styling when anoth